@smartbear/mcp 0.4.0 → 0.5.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/README.md +20 -20
- package/dist/{insight-hub → bugsnag}/client/api/CurrentUser.js +2 -1
- package/dist/{insight-hub → bugsnag}/client/api/Error.js +37 -4
- package/dist/{insight-hub → bugsnag}/client/api/Project.js +2 -1
- package/dist/{insight-hub → bugsnag}/client/api/base.js +18 -1
- package/dist/{insight-hub → bugsnag}/client/api/filters.js +2 -2
- package/dist/{insight-hub → bugsnag}/client.js +87 -19
- package/dist/common/info.js +1 -1
- package/dist/common/server.js +8 -5
- package/dist/index.js +9 -9
- package/dist/pactflow/client/ai.js +24 -5
- package/dist/pactflow/client/base.js +6 -1
- package/dist/pactflow/client/tools.js +9 -0
- package/dist/pactflow/client/utils.js +70 -0
- package/dist/pactflow/client.js +58 -10
- package/package.json +6 -3
- package/dist/package.json +0 -60
- package/dist/tests/unit/common/server.test.js +0 -319
- package/dist/tests/unit/insight-hub/api-utilities.test.js +0 -31
- package/dist/tests/unit/insight-hub/client.test.js +0 -852
- package/dist/tests/unit/insight-hub/filters.test.js +0 -93
- package/dist/tests/unit/pactflow/ai.test.js +0 -21
- package/dist/tests/unit/pactflow/client.test.js +0 -67
- package/dist/tests/unit/pactflow/tools.test.js +0 -34
- package/dist/vitest.config.js +0 -57
- /package/dist/{insight-hub → bugsnag}/client/api/index.js +0 -0
- /package/dist/{insight-hub → bugsnag}/client/configuration.js +0 -0
- /package/dist/{insight-hub → bugsnag}/client/index.js +0 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { RemoteOpenAPIDocumentSchema, } from "./ai.js";
|
|
2
|
+
import yaml from "js-yaml";
|
|
3
|
+
// @ts-expect-error missing type declarations
|
|
4
|
+
import Swagger from "swagger-client";
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the OpenAPI specification from the provided input.
|
|
7
|
+
*
|
|
8
|
+
* @param remoteOpenAPIDocument The remote OpenAPI document to resolve.
|
|
9
|
+
* @returns The resolved OpenAPI document.
|
|
10
|
+
* @throws Error if the resolution fails.
|
|
11
|
+
*/
|
|
12
|
+
export async function resolveOpenAPISpec(remoteOpenAPIDocument) {
|
|
13
|
+
const openAPISchema = RemoteOpenAPIDocumentSchema.safeParse(remoteOpenAPIDocument);
|
|
14
|
+
if (openAPISchema.error || !remoteOpenAPIDocument) {
|
|
15
|
+
throw new Error(`Invalid RemoteOpenAPIDocument: ${JSON.stringify(openAPISchema.error?.issues)}`);
|
|
16
|
+
}
|
|
17
|
+
const unresolvedSpec = await getRemoteSpecContents(openAPISchema.data);
|
|
18
|
+
const resolvedSpec = await Swagger.resolve({ spec: unresolvedSpec });
|
|
19
|
+
if (resolvedSpec.errors?.length) {
|
|
20
|
+
throw new Error(`Failed to resolve OpenAPI document: ${resolvedSpec.errors?.join(", ")}`);
|
|
21
|
+
}
|
|
22
|
+
return resolvedSpec.spec;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Fetch the contents of a remote OpenAPI document.
|
|
26
|
+
*
|
|
27
|
+
* @param openAPISchema The schema for the remote OpenAPI document.
|
|
28
|
+
* @returns A promise that resolves to a map of the OpenAPI document contents.
|
|
29
|
+
* @throws Error if the URL is not provided or the fetch fails.
|
|
30
|
+
*/
|
|
31
|
+
export async function getRemoteSpecContents(openAPISchema) {
|
|
32
|
+
if (!openAPISchema.url) {
|
|
33
|
+
throw new Error("'url' must be provided.");
|
|
34
|
+
}
|
|
35
|
+
let headers = {};
|
|
36
|
+
if (openAPISchema.authToken) {
|
|
37
|
+
headers = {
|
|
38
|
+
Authorization: `${openAPISchema.authScheme ?? "Bearer"} ${openAPISchema.authToken}`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const remoteSpec = await fetch(openAPISchema.url, {
|
|
42
|
+
headers,
|
|
43
|
+
method: "GET",
|
|
44
|
+
});
|
|
45
|
+
const specRawBody = await remoteSpec.text();
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(specRawBody);
|
|
48
|
+
}
|
|
49
|
+
catch (jsonError) {
|
|
50
|
+
try {
|
|
51
|
+
return yaml.load(specRawBody);
|
|
52
|
+
}
|
|
53
|
+
catch (yamlError) {
|
|
54
|
+
throw new Error(`Unsupported Content-Type: ${remoteSpec.headers.get("Content-Type")} for remote OpenAPI document. Found following parse errors:-\nJSON parse error: ${jsonError}\nYAML parse error: ${yamlError}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Adds the OpenAPI specification to the input schema if a remote document is provided.
|
|
60
|
+
*
|
|
61
|
+
* @param inputSchema The input schema to modify.
|
|
62
|
+
* @returns The modified input schema with the OpenAPI specification added.
|
|
63
|
+
*/
|
|
64
|
+
export async function addOpenAPISpecToSchema(inputSchema) {
|
|
65
|
+
if (inputSchema.remoteDocument) {
|
|
66
|
+
const resolvedSpec = await resolveOpenAPISpec(inputSchema.remoteDocument);
|
|
67
|
+
inputSchema.document = resolvedSpec;
|
|
68
|
+
}
|
|
69
|
+
return inputSchema;
|
|
70
|
+
}
|
package/dist/pactflow/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { TOOLS } from "./client/tools.js";
|
|
2
1
|
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
|
|
2
|
+
import { TOOLS } from "./client/tools.js";
|
|
3
3
|
// Tool definitions for PactFlow AI API client
|
|
4
4
|
export class PactflowClient {
|
|
5
5
|
name = "Contract Testing";
|
|
@@ -30,28 +30,42 @@ export class PactflowClient {
|
|
|
30
30
|
this.clientType = clientType;
|
|
31
31
|
}
|
|
32
32
|
// PactFlow AI client methods
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Generate new Pact tests based on the provided input.
|
|
35
|
+
*
|
|
36
|
+
* @param toolInput The input data for the generation process.
|
|
37
|
+
* @returns The result of the generation process.
|
|
38
|
+
* @throws Error if the HTTP request fails or the operation times out.
|
|
39
|
+
*/
|
|
40
|
+
async generate(toolInput) {
|
|
34
41
|
// Submit the generation request
|
|
35
42
|
const response = await fetch(`${this.aiBaseUrl}/generate`, {
|
|
36
43
|
method: "POST",
|
|
37
44
|
headers: this.headers,
|
|
38
|
-
body: JSON.stringify(
|
|
45
|
+
body: JSON.stringify(toolInput),
|
|
39
46
|
});
|
|
40
47
|
if (!response.ok) {
|
|
41
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
48
|
+
throw new Error(`HTTP error! status: ${response.status} - ${await response.text()}`);
|
|
42
49
|
}
|
|
43
50
|
const status_response = await response.json();
|
|
44
51
|
return await this.pollForCompletion(status_response, "Generation");
|
|
45
52
|
}
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Review the provided Pact tests and suggest improvements.
|
|
55
|
+
*
|
|
56
|
+
* @param toolInput The input data for the review process.
|
|
57
|
+
* @returns The result of the review process.
|
|
58
|
+
* @throws Error if the HTTP request fails or the operation times out.
|
|
59
|
+
*/
|
|
60
|
+
async review(toolInput) {
|
|
61
|
+
// Submit review request
|
|
48
62
|
const response = await fetch(`${this.aiBaseUrl}/review`, {
|
|
49
63
|
method: "POST",
|
|
50
64
|
headers: this.headers,
|
|
51
|
-
body: JSON.stringify(
|
|
65
|
+
body: JSON.stringify(toolInput),
|
|
52
66
|
});
|
|
53
67
|
if (!response.ok) {
|
|
54
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
68
|
+
throw new Error(`HTTP error! status: ${response.status} - ${await response.text()}`);
|
|
55
69
|
}
|
|
56
70
|
const status_response = await response.json();
|
|
57
71
|
return await this.pollForCompletion(status_response, "Review Pacts");
|
|
@@ -108,10 +122,44 @@ export class PactflowClient {
|
|
|
108
122
|
}
|
|
109
123
|
return response.json();
|
|
110
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Checks if a given pacticipant version is safe to deploy
|
|
127
|
+
* to a specified environment.
|
|
128
|
+
*
|
|
129
|
+
* @param body - Input containing:
|
|
130
|
+
* - `pacticipant`: The name of the service (pacticipant).
|
|
131
|
+
* - `version`: The version of the pacticipant being evaluated for deployment.
|
|
132
|
+
* - `environment`: The target environment (e.g., staging, production).
|
|
133
|
+
* @returns CanIDeployResponse containing deployment decision and verification results.
|
|
134
|
+
* @throws Error if the request fails or returns a non-OK response.
|
|
135
|
+
*/
|
|
136
|
+
async canIDeploy(body) {
|
|
137
|
+
const { pacticipant, version, environment } = body;
|
|
138
|
+
const queryParams = new URLSearchParams({
|
|
139
|
+
pacticipant,
|
|
140
|
+
version,
|
|
141
|
+
environment,
|
|
142
|
+
});
|
|
143
|
+
const url = `${this.baseUrl}/can-i-deploy?${queryParams.toString()}`;
|
|
144
|
+
try {
|
|
145
|
+
const response = await fetch(url, {
|
|
146
|
+
method: "GET",
|
|
147
|
+
headers: this.headers,
|
|
148
|
+
});
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
const errorText = await response.text().catch(() => "");
|
|
151
|
+
throw new Error(`Can-I-Deploy Request Failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
|
|
152
|
+
}
|
|
153
|
+
return (await response.json());
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.error("[CanIDeploy] Unexpected error:", error);
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
111
160
|
registerTools(register, _getInput) {
|
|
112
161
|
for (const tool of TOOLS.filter(t => t.clients.includes(this.clientType))) {
|
|
113
|
-
const { handler, clients, formatResponse, ...toolparams } = tool;
|
|
114
|
-
console.log(clients);
|
|
162
|
+
const { handler, clients: _, formatResponse, ...toolparams } = tool; // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
115
163
|
register(toolparams, async (args, _extra) => {
|
|
116
164
|
const handler_fn = this[handler];
|
|
117
165
|
if (typeof handler_fn !== "function") {
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartbear/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "MCP server for interacting SmartBear Products",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"smartbear",
|
|
7
7
|
"mcp",
|
|
8
|
-
"
|
|
8
|
+
"bugsnag",
|
|
9
9
|
"reflect",
|
|
10
10
|
"api-hub",
|
|
11
11
|
"pactflow"
|
|
@@ -44,10 +44,12 @@
|
|
|
44
44
|
"@bugsnag/js": "^8.2.0",
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
46
46
|
"node-cache": "^5.1.2",
|
|
47
|
+
"swagger-client": "^3.35.6",
|
|
47
48
|
"zod": "^3"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@eslint/js": "^9.29.0",
|
|
52
|
+
"@types/js-yaml": "^4.0.9",
|
|
51
53
|
"@types/node": "^22",
|
|
52
54
|
"@vitest/coverage-v8": "^3.2.4",
|
|
53
55
|
"eslint": "^9.29.0",
|
|
@@ -55,6 +57,7 @@
|
|
|
55
57
|
"shx": "^0.3.4",
|
|
56
58
|
"typescript": "^5.6.2",
|
|
57
59
|
"typescript-eslint": "^8.34.1",
|
|
58
|
-
"vitest": "^3.2.4"
|
|
60
|
+
"vitest": "^3.2.4",
|
|
61
|
+
"vitest-fetch-mock": "^0.4.5"
|
|
59
62
|
}
|
|
60
63
|
}
|
package/dist/package.json
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@smartbear/mcp",
|
|
3
|
-
"version": "0.4.0",
|
|
4
|
-
"description": "MCP server for interacting SmartBear Products",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"smartbear",
|
|
7
|
-
"mcp",
|
|
8
|
-
"insight-hub",
|
|
9
|
-
"reflect",
|
|
10
|
-
"api-hub",
|
|
11
|
-
"pactflow"
|
|
12
|
-
],
|
|
13
|
-
"homepage": "https://developer.smartbear.com/smartbear-mcp",
|
|
14
|
-
"repository": {
|
|
15
|
-
"type": "git",
|
|
16
|
-
"url": "git@github.com:SmartBear/smartbear-mcp.git"
|
|
17
|
-
},
|
|
18
|
-
"license": "MIT",
|
|
19
|
-
"type": "module",
|
|
20
|
-
"bin": {
|
|
21
|
-
"mcp": "dist/index.js"
|
|
22
|
-
},
|
|
23
|
-
"files": [
|
|
24
|
-
"dist",
|
|
25
|
-
"assets",
|
|
26
|
-
"**/README.md"
|
|
27
|
-
],
|
|
28
|
-
"config": {
|
|
29
|
-
"mcpServerName": "SmartBear MCP Server"
|
|
30
|
-
},
|
|
31
|
-
"scripts": {
|
|
32
|
-
"build": "tsc && shx chmod +x dist/*.js",
|
|
33
|
-
"lint": "eslint . --ext .ts",
|
|
34
|
-
"prepare": "npm run build",
|
|
35
|
-
"watch": "tsc --watch",
|
|
36
|
-
"test": "vitest",
|
|
37
|
-
"test:watch": "vitest --watch",
|
|
38
|
-
"test:coverage": "vitest --coverage",
|
|
39
|
-
"test:coverage:ci": "vitest --coverage --reporter=verbose",
|
|
40
|
-
"test:run": "vitest run",
|
|
41
|
-
"coverage:check": "vitest --coverage --reporter=verbose --config vitest.config.coverage.ts"
|
|
42
|
-
},
|
|
43
|
-
"dependencies": {
|
|
44
|
-
"@bugsnag/js": "^8.2.0",
|
|
45
|
-
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
46
|
-
"node-cache": "^5.1.2",
|
|
47
|
-
"zod": "^3"
|
|
48
|
-
},
|
|
49
|
-
"devDependencies": {
|
|
50
|
-
"@eslint/js": "^9.29.0",
|
|
51
|
-
"@types/node": "^22",
|
|
52
|
-
"@vitest/coverage-v8": "^3.2.4",
|
|
53
|
-
"eslint": "^9.29.0",
|
|
54
|
-
"globals": "^16.2.0",
|
|
55
|
-
"shx": "^0.3.4",
|
|
56
|
-
"typescript": "^5.6.2",
|
|
57
|
-
"typescript-eslint": "^8.34.1",
|
|
58
|
-
"vitest": "^3.2.4"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
@@ -1,319 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,31 +0,0 @@
|
|
|
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
|
-
});
|