@smartbear/mcp 0.2.2 → 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.
@@ -0,0 +1,319 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { SmartBearMcpServer } from "../../../common/server.js";
3
+ import z from "zod";
4
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import Bugsnag from "../../../common/bugsnag.js";
6
+ // Mock Bugsnag
7
+ vi.mock("../../../common/bugsnag.js", () => ({
8
+ default: {
9
+ notify: vi.fn(),
10
+ },
11
+ }));
12
+ describe("SmartBearMcpServer", () => {
13
+ let server;
14
+ let superRegisterToolMock;
15
+ let superRegisterResourceMock;
16
+ beforeEach(() => {
17
+ server = new SmartBearMcpServer();
18
+ // This approach is required to mock the super call - other techniques result in mocking the actual server
19
+ superRegisterToolMock = vi
20
+ .spyOn(Object.getPrototypeOf(Object.getPrototypeOf(server)), "registerTool")
21
+ .mockImplementation(vi.fn());
22
+ superRegisterResourceMock = vi
23
+ .spyOn(Object.getPrototypeOf(Object.getPrototypeOf(server)), "registerResource")
24
+ .mockImplementation(vi.fn());
25
+ server.server.elicitInput = vi.fn().mockResolvedValue("mocked input");
26
+ // Reset the Bugsnag mock
27
+ vi.mocked(Bugsnag.notify).mockClear();
28
+ });
29
+ describe("getDescription tests", () => {
30
+ it("should return the correct description", () => {
31
+ const zSchema = z.object({
32
+ examples: z.enum(["test_1", "test_2"]),
33
+ constraints: z.enum(["test_1", "test_2"]),
34
+ });
35
+ const toolparams = {
36
+ title: "Test Tool",
37
+ summary: "A test tool",
38
+ zodSchema: zSchema,
39
+ };
40
+ const description = server["getDescription"](toolparams);
41
+ expect(description).toBe(`A test tool
42
+
43
+ **Parameters:**
44
+ - examples (enum) *required* (e.g. test_1, test_2)
45
+ - constraints (enum) *required*
46
+ - test_1
47
+ - test_2`);
48
+ });
49
+ });
50
+ describe("addClient", () => {
51
+ let mockClient;
52
+ beforeEach(() => {
53
+ mockClient = {
54
+ name: "Test Product",
55
+ prefix: "test_product",
56
+ registerTools: vi.fn(),
57
+ registerResources: vi.fn(),
58
+ };
59
+ });
60
+ it("should register tools when client provides them", async () => {
61
+ server.addClient(mockClient);
62
+ // The server should call the client's registerTools function
63
+ expect(mockClient.registerTools).toHaveBeenCalledWith(expect.any(Function), expect.any(Function));
64
+ // Get the register function passed from the server and execute it with test tool details
65
+ const registerFn = mockClient.registerTools.mock.calls[0][0];
66
+ const registerCbMock = vi.fn();
67
+ registerFn({
68
+ title: "Test Tool",
69
+ summary: "A test tool",
70
+ parameters: [
71
+ {
72
+ name: "p1",
73
+ type: z.string(),
74
+ required: true,
75
+ description: "The input for the tool",
76
+ },
77
+ ],
78
+ }, registerCbMock);
79
+ expect(superRegisterToolMock).toHaveBeenCalledOnce();
80
+ // Assert some of the details
81
+ const registerToolParams = superRegisterToolMock.mock.calls[0];
82
+ expect(registerToolParams[0]).toBe("test_product_test_tool");
83
+ expect(registerToolParams[1]["title"]).toBe("Test Product: Test Tool");
84
+ expect(registerToolParams[1]["description"]).toBe("A test tool\n\n" +
85
+ "**Parameters:**\n" +
86
+ "- p1 (string) *required*: The input for the tool");
87
+ expect(registerToolParams[1]["inputSchema"]["p1"].toString()).toBe(z.string().describe("The input for the tool").toString());
88
+ expect(registerToolParams[1]["annotations"]).toEqual({
89
+ title: "Test Product: Test Tool",
90
+ readOnlyHint: true,
91
+ destructiveHint: false,
92
+ idempotentHint: true,
93
+ openWorldHint: false,
94
+ });
95
+ // Get the wrapper function that will execute the tool and call it
96
+ registerToolParams[2]();
97
+ expect(registerCbMock).toHaveBeenCalledOnce();
98
+ expect(vi.mocked(Bugsnag.notify)).not.toHaveBeenCalled();
99
+ });
100
+ it("should register tools with complex parameters", async () => {
101
+ server.addClient(mockClient);
102
+ // Get the register function passed from the server and execute it with test tool details
103
+ const registerFn = mockClient.registerTools.mock.calls[0][0];
104
+ registerFn({
105
+ title: "Test Tool",
106
+ summary: "A test tool",
107
+ parameters: [
108
+ {
109
+ name: "p1",
110
+ type: z.string(),
111
+ required: true,
112
+ description: "The input for the tool",
113
+ examples: ["example1", "example2"],
114
+ constraints: ["constraint1", "constraint2"],
115
+ },
116
+ {
117
+ name: "p2",
118
+ type: z.number(),
119
+ required: false,
120
+ description: "The optional numeric input for the tool",
121
+ },
122
+ {
123
+ name: "p3",
124
+ type: z.boolean(),
125
+ required: true,
126
+ },
127
+ {
128
+ name: "p4",
129
+ type: z.array(z.string()),
130
+ required: true,
131
+ },
132
+ {
133
+ name: "p5",
134
+ type: z.object({
135
+ key1: z.string(),
136
+ key2: z.number(),
137
+ }),
138
+ required: true,
139
+ },
140
+ {
141
+ name: "p6",
142
+ type: z.enum(["value1", "value2", "value3"]),
143
+ required: true,
144
+ },
145
+ {
146
+ name: "p7",
147
+ type: z.literal("value"),
148
+ required: true,
149
+ },
150
+ {
151
+ name: "p8",
152
+ type: z.union([z.literal("value1"), z.literal("value2")]),
153
+ required: true,
154
+ },
155
+ {
156
+ name: "p9",
157
+ type: z.any(),
158
+ required: true,
159
+ },
160
+ ],
161
+ purpose: "To test the tool registration process",
162
+ useCases: ["Testing", "Development"],
163
+ examples: [
164
+ {
165
+ description: "Example 1",
166
+ parameters: {
167
+ p1: "example1",
168
+ p2: 42,
169
+ },
170
+ expectedOutput: "Expected output for example 1",
171
+ },
172
+ {
173
+ description: "Example 2",
174
+ parameters: {
175
+ p1: "example2",
176
+ p2: 24,
177
+ },
178
+ },
179
+ ],
180
+ hints: ["First hint", "Second hint"],
181
+ outputFormat: "The output format",
182
+ readOnly: true,
183
+ destructive: true,
184
+ idempotent: true,
185
+ openWorld: true,
186
+ }, vi.fn());
187
+ // Assert some of the details
188
+ const registerToolParams = superRegisterToolMock.mock.calls[0];
189
+ expect(registerToolParams[0]).toBe("test_product_test_tool");
190
+ expect(registerToolParams[1]["title"]).toBe("Test Product: Test Tool");
191
+ expect(registerToolParams[1]["description"]).toBe("A test tool\n\n" +
192
+ "**Parameters:**\n" +
193
+ "- p1 (string) *required*: The input for the tool (e.g. example1, example2)\n" +
194
+ " - constraint1\n" +
195
+ " - constraint2\n" +
196
+ "- p2 (number): The optional numeric input for the tool\n" +
197
+ "- p3 (boolean) *required*\n" +
198
+ "- p4 (array) *required*\n" +
199
+ "- p5 (object) *required*\n" +
200
+ "- p6 (enum) *required*\n" +
201
+ "- p7 (literal) *required*\n" +
202
+ "- p8 (union) *required*\n" +
203
+ "- p9 (any) *required*\n\n" +
204
+ "**Output Format:** The output format\n\n" +
205
+ "**Use Cases:** 1. Testing 2. Development\n\n" +
206
+ "**Examples:**\n" +
207
+ "1. Example 1\n" +
208
+ "```json\n" +
209
+ "{\n" +
210
+ ' "p1": "example1",\n' +
211
+ ' "p2": 42\n' +
212
+ "}\n" +
213
+ "```\n" +
214
+ "Expected Output: Expected output for example 1\n\n" +
215
+ "2. Example 2\n" +
216
+ "```json\n" +
217
+ "{\n" +
218
+ ' "p1": "example2",\n' +
219
+ ' "p2": 24\n' +
220
+ "}\n" +
221
+ "```\n\n" +
222
+ "**Hints:** 1. First hint 2. Second hint");
223
+ expect(registerToolParams[1]["inputSchema"]["p1"].toString()).toBe(z.string().describe("The input for the tool").toString());
224
+ expect(registerToolParams[1]["annotations"]).toEqual({
225
+ title: "Test Product: Test Tool",
226
+ readOnlyHint: true,
227
+ destructiveHint: true,
228
+ idempotentHint: true,
229
+ openWorldHint: true,
230
+ });
231
+ });
232
+ it("should handle errors when registering tools", async () => {
233
+ server.addClient(mockClient);
234
+ // Get the register function passed from the server and execute it with test tool details
235
+ const registerFn = mockClient.registerTools.mock.calls[0][0];
236
+ const registerCbMock = vi.fn();
237
+ registerFn({
238
+ title: "Test Tool",
239
+ summary: "A test tool",
240
+ parameters: [],
241
+ }, registerCbMock);
242
+ // Make the callback throw an error to test error handling
243
+ registerCbMock.mockImplementation(() => {
244
+ throw new Error("Test error from registerCbMock");
245
+ });
246
+ // Get the wrapper function that will execute the tool and call it
247
+ const registerToolParams = superRegisterToolMock.mock.calls[0];
248
+ await expect(registerToolParams[2]()).rejects.toThrow("Test error from registerCbMock");
249
+ expect(registerCbMock).toHaveBeenCalledOnce();
250
+ expect(vi.mocked(Bugsnag.notify)).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({
251
+ message: "Test error from registerCbMock",
252
+ }));
253
+ });
254
+ it("should elicit input when client requires it", async () => {
255
+ server.addClient(mockClient);
256
+ // The server should call the client's registerTools function
257
+ expect(mockClient.registerTools).toHaveBeenCalledWith(expect.any(Function), expect.any(Function));
258
+ // Get the register function passed from the server and execute it with test tool details
259
+ const getInputFn = mockClient.registerTools.mock.calls[0][1];
260
+ const params = vi.mockObject({});
261
+ const options = vi.mockObject({});
262
+ getInputFn(params, options);
263
+ expect(server.server.elicitInput).toHaveBeenCalledExactlyOnceWith(params, options);
264
+ });
265
+ it("should register resources when client provides them", async () => {
266
+ const mockClient = {
267
+ name: "Test Product",
268
+ prefix: "test_product",
269
+ registerTools: vi.fn(),
270
+ registerResources: vi.fn(),
271
+ };
272
+ server.addClient(mockClient);
273
+ // The server should call the client's registerResources function
274
+ expect(mockClient.registerResources).toHaveBeenCalledWith(expect.any(Function));
275
+ // Get the register function passed from the server and execute it with test resource details
276
+ const registerFn = mockClient.registerResources.mock.calls[0][0];
277
+ const registerCbMock = vi.fn();
278
+ registerFn("test_resource", "{identifier}", registerCbMock);
279
+ expect(superRegisterResourceMock).toHaveBeenCalledExactlyOnceWith(expect.any(String), expect.any(ResourceTemplate), expect.any(Object), expect.any(Function));
280
+ // Assert some of the details
281
+ const registerResourceParams = superRegisterResourceMock.mock.calls[0];
282
+ expect(registerResourceParams[0]).toBe("test_resource");
283
+ expect(registerResourceParams[1].uriTemplate.template).toBe("test_product://test_resource/{identifier}");
284
+ // Get the wrapper function that will execute the tool and call it
285
+ registerResourceParams[3]();
286
+ expect(registerCbMock).toHaveBeenCalledOnce();
287
+ expect(vi.mocked(Bugsnag.notify)).not.toHaveBeenCalled();
288
+ });
289
+ it("should not register resources when client does not provide them", async () => {
290
+ const mockClient = {
291
+ name: "Test Product",
292
+ prefix: "test_product",
293
+ registerTools: vi.fn(),
294
+ registerResources: undefined,
295
+ };
296
+ server.addClient(mockClient);
297
+ // It should not crash with undefined registerResources function
298
+ expect(vi.mocked(Bugsnag.notify)).not.toHaveBeenCalled();
299
+ });
300
+ it("should handle errors when registering resources", async () => {
301
+ server.addClient(mockClient);
302
+ // Get the register function passed from the server and execute it with test resource details
303
+ const registerFn = mockClient.registerResources.mock.calls[0][0];
304
+ const registerCbMock = vi.fn();
305
+ registerFn("test_resource", "{identifier}", registerCbMock);
306
+ // Make the callback throw an error to test error handling
307
+ registerCbMock.mockImplementation(() => {
308
+ throw new Error("Test error from registerCbMock");
309
+ });
310
+ // Get the wrapper function that will execute the resource and call it
311
+ const registerResourceParams = superRegisterResourceMock.mock.calls[0];
312
+ await expect(registerResourceParams[3]()).rejects.toThrow("Test error from registerCbMock");
313
+ expect(registerCbMock).toHaveBeenCalledOnce();
314
+ expect(vi.mocked(Bugsnag.notify)).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({
315
+ message: "Test error from registerCbMock",
316
+ }));
317
+ });
318
+ });
319
+ });
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { pickFields, pickFieldsFromArray } from '../../../insight-hub/client/api/base.js';
3
+ describe('API Utilities', () => {
4
+ describe('pickFields', () => {
5
+ it('should pick only specified fields from object', () => {
6
+ const input = { id: '1', name: 'Test', secret: 'hidden', extra: 'data' };
7
+ const result = pickFields(input, ['id', 'name']);
8
+ expect(result).toEqual({ id: '1', name: 'Test' });
9
+ expect(result).not.toHaveProperty('secret');
10
+ expect(result).not.toHaveProperty('extra');
11
+ });
12
+ it('should handle missing fields gracefully', () => {
13
+ const input = { id: '1' };
14
+ const result = pickFields(input, ['id', 'name', 'missing']);
15
+ expect(result).toEqual({ id: '1' });
16
+ });
17
+ });
18
+ describe('pickFieldsFromArray', () => {
19
+ it('should pick fields from array of objects', () => {
20
+ const input = [
21
+ { id: '1', name: 'Test1', secret: 'hidden1' },
22
+ { id: '2', name: 'Test2', secret: 'hidden2' }
23
+ ];
24
+ const result = pickFieldsFromArray(input, ['id', 'name']);
25
+ expect(result).toEqual([
26
+ { id: '1', name: 'Test1' },
27
+ { id: '2', name: 'Test2' }
28
+ ]);
29
+ });
30
+ });
31
+ });