mcp-server-diff 2.1.0 → 2.1.5

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.
@@ -1,430 +0,0 @@
1
- import { parseHeaders, parseConfigurations } from "../runner.js";
2
- import { normalizeProbeResult } from "../probe.js";
3
-
4
- describe("parseHeaders", () => {
5
- it("returns empty object for empty input", () => {
6
- expect(parseHeaders("")).toEqual({});
7
- expect(parseHeaders(" ")).toEqual({});
8
- });
9
-
10
- it("parses JSON object format", () => {
11
- const input = '{"Authorization": "Bearer token123", "X-Custom": "value"}';
12
- const result = parseHeaders(input);
13
- expect(result).toEqual({
14
- Authorization: "Bearer token123",
15
- "X-Custom": "value",
16
- });
17
- });
18
-
19
- it("parses colon-separated header format", () => {
20
- const input = `Authorization: Bearer token123
21
- X-Custom: value`;
22
- const result = parseHeaders(input);
23
- expect(result).toEqual({
24
- Authorization: "Bearer token123",
25
- "X-Custom": "value",
26
- });
27
- });
28
-
29
- it("handles headers with colons in values", () => {
30
- const input = "X-Timestamp: 2024:01:15:12:00:00";
31
- const result = parseHeaders(input);
32
- expect(result).toEqual({
33
- "X-Timestamp": "2024:01:15:12:00:00",
34
- });
35
- });
36
-
37
- it("skips empty lines in colon-separated format", () => {
38
- const input = `Authorization: Bearer token
39
-
40
- X-Custom: value`;
41
- const result = parseHeaders(input);
42
- expect(result).toEqual({
43
- Authorization: "Bearer token",
44
- "X-Custom": "value",
45
- });
46
- });
47
-
48
- it("trims whitespace from header names and values", () => {
49
- const input = " Authorization : Bearer token ";
50
- const result = parseHeaders(input);
51
- expect(result).toEqual({
52
- Authorization: "Bearer token",
53
- });
54
- });
55
-
56
- it("returns empty object for invalid JSON that doesn't look like headers", () => {
57
- const input = "not valid headers or json";
58
- const result = parseHeaders(input);
59
- expect(result).toEqual({});
60
- });
61
- });
62
-
63
- describe("parseConfigurations", () => {
64
- const defaultTransport = "stdio" as const;
65
- const defaultCommand = "node server.js";
66
- const defaultUrl = "http://localhost:3000/mcp";
67
-
68
- it("returns default config when input is empty", () => {
69
- const result = parseConfigurations("", defaultTransport, defaultCommand, defaultUrl);
70
- expect(result).toHaveLength(1);
71
- expect(result[0]).toEqual({
72
- name: "default",
73
- transport: "stdio",
74
- start_command: "node server.js",
75
- server_url: undefined,
76
- });
77
- });
78
-
79
- it("returns default config when input is empty array", () => {
80
- const result = parseConfigurations("[]", defaultTransport, defaultCommand, defaultUrl);
81
- expect(result).toHaveLength(1);
82
- expect(result[0].name).toBe("default");
83
- });
84
-
85
- it("applies transport default to configs without transport", () => {
86
- const input = JSON.stringify([
87
- { name: "test1", args: "--read-only" },
88
- { name: "test2", args: "--dynamic" },
89
- ]);
90
- const result = parseConfigurations(input, defaultTransport, defaultCommand, defaultUrl);
91
-
92
- expect(result).toHaveLength(2);
93
- expect(result[0].transport).toBe("stdio");
94
- expect(result[1].transport).toBe("stdio");
95
- });
96
-
97
- it("applies start_command default to stdio configs without start_command", () => {
98
- const input = JSON.stringify([
99
- { name: "test1", args: "--read-only" },
100
- { name: "test2", start_command: "custom command" },
101
- ]);
102
- const result = parseConfigurations(input, defaultTransport, defaultCommand, defaultUrl);
103
-
104
- expect(result[0].start_command).toBe("node server.js");
105
- expect(result[1].start_command).toBe("custom command");
106
- });
107
-
108
- it("applies server_url default to http configs without server_url", () => {
109
- const input = JSON.stringify([
110
- { name: "test1", transport: "streamable-http" },
111
- { name: "test2", transport: "streamable-http", server_url: "http://custom:8080/mcp" },
112
- ]);
113
- const result = parseConfigurations(input, defaultTransport, defaultCommand, defaultUrl);
114
-
115
- expect(result[0].server_url).toBe("http://localhost:3000/mcp");
116
- expect(result[1].server_url).toBe("http://custom:8080/mcp");
117
- });
118
-
119
- it("preserves explicit transport values", () => {
120
- const input = JSON.stringify([
121
- { name: "stdio-test", transport: "stdio", start_command: "node server.js" },
122
- { name: "http-test", transport: "streamable-http", server_url: "http://localhost:3000" },
123
- ]);
124
- const result = parseConfigurations(input, defaultTransport, defaultCommand, defaultUrl);
125
-
126
- expect(result[0].transport).toBe("stdio");
127
- expect(result[1].transport).toBe("streamable-http");
128
- });
129
-
130
- it("handles github-mcp-server style configs (name + args only)", () => {
131
- const input = JSON.stringify([
132
- { name: "default", args: "" },
133
- { name: "read-only", args: "--read-only" },
134
- { name: "dynamic-toolsets", args: "--dynamic-toolsets" },
135
- ]);
136
- const result = parseConfigurations(input, "stdio", "go run ./cmd/server stdio", defaultUrl);
137
-
138
- expect(result).toHaveLength(3);
139
- expect(result[0]).toMatchObject({
140
- name: "default",
141
- transport: "stdio",
142
- start_command: "go run ./cmd/server stdio",
143
- });
144
- expect(result[1]).toMatchObject({
145
- name: "read-only",
146
- transport: "stdio",
147
- start_command: "go run ./cmd/server stdio",
148
- args: "--read-only",
149
- });
150
- });
151
- });
152
-
153
- describe("normalizeProbeResult", () => {
154
- it("returns null/undefined as-is", () => {
155
- expect(normalizeProbeResult(null)).toBe(null);
156
- expect(normalizeProbeResult(undefined)).toBe(undefined);
157
- });
158
-
159
- it("returns primitives as-is", () => {
160
- expect(normalizeProbeResult("hello")).toBe("hello");
161
- expect(normalizeProbeResult(123)).toBe(123);
162
- expect(normalizeProbeResult(true)).toBe(true);
163
- });
164
-
165
- it("sorts object keys alphabetically", () => {
166
- const input = { zebra: 1, apple: 2, mango: 3 };
167
- const result = normalizeProbeResult(input);
168
- const keys = Object.keys(result as object);
169
- expect(keys).toEqual(["apple", "mango", "zebra"]);
170
- });
171
-
172
- it("sorts nested object keys", () => {
173
- const input = {
174
- outer: {
175
- zebra: 1,
176
- apple: 2,
177
- },
178
- };
179
- const result = normalizeProbeResult(input) as { outer: object };
180
- const nestedKeys = Object.keys(result.outer);
181
- expect(nestedKeys).toEqual(["apple", "zebra"]);
182
- });
183
-
184
- it("sorts arrays of objects by 'name' field (tools)", () => {
185
- const input = {
186
- tools: [
187
- { name: "zebra_tool", description: "Z tool" },
188
- { name: "apple_tool", description: "A tool" },
189
- { name: "mango_tool", description: "M tool" },
190
- ],
191
- };
192
- const result = normalizeProbeResult(input) as { tools: Array<{ name: string }> };
193
- expect(result.tools[0].name).toBe("apple_tool");
194
- expect(result.tools[1].name).toBe("mango_tool");
195
- expect(result.tools[2].name).toBe("zebra_tool");
196
- });
197
-
198
- it("sorts arrays of objects by 'uri' field (resources)", () => {
199
- const input = {
200
- resources: [
201
- { uri: "file:///z.txt", name: "Z" },
202
- { uri: "file:///a.txt", name: "A" },
203
- { uri: "file:///m.txt", name: "M" },
204
- ],
205
- };
206
- const result = normalizeProbeResult(input) as { resources: Array<{ uri: string }> };
207
- expect(result.resources[0].uri).toBe("file:///a.txt");
208
- expect(result.resources[1].uri).toBe("file:///m.txt");
209
- expect(result.resources[2].uri).toBe("file:///z.txt");
210
- });
211
-
212
- it("sorts arrays of objects by 'uriTemplate' field (resource templates)", () => {
213
- const input = {
214
- resourceTemplates: [
215
- { uriTemplate: "file:///{z}", name: "Z Template" },
216
- { uriTemplate: "file:///{a}", name: "A Template" },
217
- ],
218
- };
219
- const result = normalizeProbeResult(input) as {
220
- resourceTemplates: Array<{ uriTemplate: string }>;
221
- };
222
- expect(result.resourceTemplates[0].uriTemplate).toBe("file:///{a}");
223
- expect(result.resourceTemplates[1].uriTemplate).toBe("file:///{z}");
224
- });
225
-
226
- it("sorts arrays of objects by 'type' field (content items)", () => {
227
- const input = {
228
- content: [
229
- { type: "text", text: "Hello" },
230
- { type: "image", data: "base64..." },
231
- { type: "audio", data: "base64..." },
232
- ],
233
- };
234
- const result = normalizeProbeResult(input) as { content: Array<{ type: string }> };
235
- expect(result.content[0].type).toBe("audio");
236
- expect(result.content[1].type).toBe("image");
237
- expect(result.content[2].type).toBe("text");
238
- });
239
-
240
- it("sorts prompt arguments by name", () => {
241
- const input = {
242
- prompts: [
243
- {
244
- name: "test-prompt",
245
- arguments: [
246
- { name: "zebra_arg", required: true },
247
- { name: "apple_arg", required: false },
248
- { name: "mango_arg", required: true },
249
- ],
250
- },
251
- ],
252
- };
253
- const result = normalizeProbeResult(input) as {
254
- prompts: Array<{ arguments: Array<{ name: string }> }>;
255
- };
256
- const args = result.prompts[0].arguments;
257
- expect(args[0].name).toBe("apple_arg");
258
- expect(args[1].name).toBe("mango_arg");
259
- expect(args[2].name).toBe("zebra_arg");
260
- });
261
-
262
- it("sorts tool inputSchema properties deterministically", () => {
263
- const input = {
264
- tools: [
265
- {
266
- name: "my_tool",
267
- inputSchema: {
268
- type: "object",
269
- properties: {
270
- zebra: { type: "string" },
271
- apple: { type: "number" },
272
- },
273
- required: ["zebra", "apple"],
274
- },
275
- },
276
- ],
277
- };
278
- const result = normalizeProbeResult(input) as {
279
- tools: Array<{ inputSchema: { properties: Record<string, unknown>; required: string[] } }>;
280
- };
281
- const propKeys = Object.keys(result.tools[0].inputSchema.properties);
282
- expect(propKeys).toEqual(["apple", "zebra"]);
283
- // Required array should also be sorted
284
- expect(result.tools[0].inputSchema.required).toEqual(["apple", "zebra"]);
285
- });
286
-
287
- it("handles embedded JSON in text fields", () => {
288
- const embeddedJson = JSON.stringify({ zebra: 1, apple: 2 });
289
- const input = {
290
- content: [{ type: "text", text: embeddedJson }],
291
- };
292
- const result = normalizeProbeResult(input) as {
293
- content: Array<{ text: string }>;
294
- };
295
- // The embedded JSON should be normalized (keys sorted)
296
- const parsed = JSON.parse(result.content[0].text);
297
- const keys = Object.keys(parsed);
298
- expect(keys).toEqual(["apple", "zebra"]);
299
- });
300
-
301
- it("handles embedded JSON arrays in text fields", () => {
302
- const embeddedJson = JSON.stringify([{ name: "zebra" }, { name: "apple" }]);
303
- const input = {
304
- content: [{ type: "text", text: embeddedJson }],
305
- };
306
- const result = normalizeProbeResult(input) as {
307
- content: Array<{ text: string }>;
308
- };
309
- // The embedded JSON array should be normalized and sorted
310
- const parsed = JSON.parse(result.content[0].text);
311
- expect(parsed[0].name).toBe("apple");
312
- expect(parsed[1].name).toBe("zebra");
313
- });
314
-
315
- it("leaves non-JSON text fields unchanged", () => {
316
- const input = {
317
- content: [{ type: "text", text: "Hello, world!" }],
318
- };
319
- const result = normalizeProbeResult(input) as {
320
- content: Array<{ text: string }>;
321
- };
322
- expect(result.content[0].text).toBe("Hello, world!");
323
- });
324
-
325
- it("produces consistent JSON output regardless of input key order", () => {
326
- const input1 = { z: 1, a: 2, m: { x: 1, b: 2 } };
327
- const input2 = { a: 2, m: { b: 2, x: 1 }, z: 1 };
328
-
329
- const result1 = JSON.stringify(normalizeProbeResult(input1));
330
- const result2 = JSON.stringify(normalizeProbeResult(input2));
331
-
332
- expect(result1).toBe(result2);
333
- });
334
-
335
- it("produces consistent JSON for complete MCP responses regardless of ordering", () => {
336
- // Simulate two identical tool responses with different initial ordering
337
- const response1 = {
338
- tools: [
339
- {
340
- name: "get_user",
341
- description: "Gets user info",
342
- inputSchema: {
343
- type: "object",
344
- required: ["id", "name"],
345
- properties: { name: { type: "string" }, id: { type: "number" } },
346
- },
347
- },
348
- {
349
- name: "add_numbers",
350
- description: "Adds two numbers",
351
- inputSchema: {
352
- type: "object",
353
- properties: { a: { type: "number" }, b: { type: "number" } },
354
- required: ["a", "b"],
355
- },
356
- },
357
- ],
358
- };
359
-
360
- const response2 = {
361
- tools: [
362
- {
363
- description: "Adds two numbers",
364
- name: "add_numbers",
365
- inputSchema: {
366
- required: ["a", "b"],
367
- type: "object",
368
- properties: { b: { type: "number" }, a: { type: "number" } },
369
- },
370
- },
371
- {
372
- inputSchema: {
373
- properties: { id: { type: "number" }, name: { type: "string" } },
374
- required: ["name", "id"],
375
- type: "object",
376
- },
377
- description: "Gets user info",
378
- name: "get_user",
379
- },
380
- ],
381
- };
382
-
383
- const normalized1 = JSON.stringify(normalizeProbeResult(response1), null, 2);
384
- const normalized2 = JSON.stringify(normalizeProbeResult(response2), null, 2);
385
-
386
- expect(normalized1).toBe(normalized2);
387
-
388
- // Verify the order is deterministic (add_numbers before get_user)
389
- const parsed = JSON.parse(normalized1) as { tools: Array<{ name: string }> };
390
- expect(parsed.tools[0].name).toBe("add_numbers");
391
- expect(parsed.tools[1].name).toBe("get_user");
392
- });
393
-
394
- it("is idempotent - normalizing twice produces same result", () => {
395
- const input = {
396
- tools: [
397
- { name: "z_tool", description: "Last" },
398
- { name: "a_tool", description: "First" },
399
- ],
400
- resources: [{ uri: "file:///z.txt" }, { uri: "file:///a.txt" }],
401
- };
402
-
403
- const once = normalizeProbeResult(input);
404
- const twice = normalizeProbeResult(once);
405
-
406
- expect(JSON.stringify(once)).toBe(JSON.stringify(twice));
407
- });
408
-
409
- it("handles arrays without identifiable sort keys", () => {
410
- const input = {
411
- data: [3, 1, 4, 1, 5, 9, 2, 6],
412
- };
413
- const result = normalizeProbeResult(input) as { data: number[] };
414
- // Numbers sorted as strings
415
- expect(result.data).toEqual([1, 1, 2, 3, 4, 5, 6, 9]);
416
- });
417
-
418
- it("handles mixed arrays with objects lacking standard keys", () => {
419
- const input = {
420
- items: [
421
- { value: 3, label: "three" },
422
- { value: 1, label: "one" },
423
- ],
424
- };
425
- const result = normalizeProbeResult(input) as { items: Array<{ value: number }> };
426
- // Falls back to JSON string comparison
427
- expect(result.items[0].value).toBe(1);
428
- expect(result.items[1].value).toBe(3);
429
- });
430
- });