@smartbear/mcp 0.1.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/LICENSE.txt +21 -0
- package/README.md +95 -0
- package/dist/api-hub/client.js +181 -0
- package/dist/common/bugsnag.js +3 -0
- package/dist/common/info.js +3 -0
- package/dist/common/types.js +1 -0
- package/dist/index.js +52 -0
- package/dist/insight-hub/client/api/CurrentUser.js +54 -0
- package/dist/insight-hub/client/api/Error.js +61 -0
- package/dist/insight-hub/client/api/base.js +54 -0
- package/dist/insight-hub/client/api/index.js +2 -0
- package/dist/insight-hub/client/configuration.js +10 -0
- package/dist/insight-hub/client/index.js +2 -0
- package/dist/insight-hub/client.js +149 -0
- package/dist/package.json +35 -0
- package/dist/reflect/client.js +141 -0
- package/package.json +35 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 SmartBear Software
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://www.smartbear.com">
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/smartbear-logo-light.svg">
|
|
5
|
+
<img alt="SmartBear logo" src="assets/smartbear-logo-dark.svg">
|
|
6
|
+
</picture>
|
|
7
|
+
</a>
|
|
8
|
+
<h1>SmartBear MCP server</h1>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
An [MCP](https://modelcontextprotocol.io) server for SmartBear's API Hub, Test Hub and Insight Hub.
|
|
12
|
+
|
|
13
|
+
## Build
|
|
14
|
+
|
|
15
|
+
Checkout this repository and run the following to build the server:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm run build
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
The server is started with the API key or auth token that you use with your product(s). They are optional and can be removed from your configuration if you aren't using the product.
|
|
24
|
+
|
|
25
|
+
### VS Code
|
|
26
|
+
|
|
27
|
+
Add the following configuration to `.vscode/mcp.json`, replacing `<PATH_TO_SMARTBEAR_MCP>` with the location of this repo on your filesystem:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"servers": {
|
|
32
|
+
"smartbear": {
|
|
33
|
+
"type": "stdio",
|
|
34
|
+
"command": "node",
|
|
35
|
+
"args": ["<PATH_TO_SMARTBEAR_MCP>/dist/index.js"],
|
|
36
|
+
|
|
37
|
+
"env": {
|
|
38
|
+
"INSIGHT_HUB_AUTH_TOKEN": "${input:insight_hub_auth_token}",
|
|
39
|
+
"REFLECT_API_TOKEN": "${input:reflect_api_token}",
|
|
40
|
+
"API_HUB_API_KEY": "${input:api_hub_api_key}"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"inputs": [
|
|
45
|
+
{
|
|
46
|
+
"id": "insight_hub_auth_token",
|
|
47
|
+
"type": "promptString",
|
|
48
|
+
"description": "Insight Hub Auth Token",
|
|
49
|
+
"password": true
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "reflect_api_token",
|
|
53
|
+
"type": "promptString",
|
|
54
|
+
"description": "Reflect API Token",
|
|
55
|
+
"password": true
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "api_hub_api_key",
|
|
59
|
+
"type": "promptString",
|
|
60
|
+
"description": "API Hub API Key",
|
|
61
|
+
"password": true
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### MCP Inspector
|
|
68
|
+
|
|
69
|
+
To test the MCP server locally, you can use the following command (assuming a local build of the MCP server in the same location):
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
REFLECT_API_TOKEN=your_reflect_token INSIGHT_HUB_AUTH_TOKEN=your_insight_hub_token API_HUB_API_KEY=your_api_hub_api_key npx @modelcontextprotocol/inspector node dist/index.js
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This will open an inspector window in your browser, where you can test the tools.
|
|
76
|
+
|
|
77
|
+
## Environment Variables
|
|
78
|
+
|
|
79
|
+
- `INSIGHT_HUB_AUTH_TOKEN`: Required for Insight Hub tools. The Auth Token for Insight Hub.
|
|
80
|
+
- `REFLECT_API_TOKEN`: Required for Reflect tools. The Reflect Account API Key for Reflect-based tools.
|
|
81
|
+
- `API_HUB_API_KEY`: Required for API Hub tools. The API Key for API Hub tools.
|
|
82
|
+
- `MCP_SERVER_INSIGHT_HUB_API_KEY`: Optional. If set, enables error reporting of the _MCP_server_ code via the BugSnag SDK. This is useful for debugging and monitoring of the MCP server itself and shouldn't be set to the same API key as your app.
|
|
83
|
+
|
|
84
|
+
## Supported Tools
|
|
85
|
+
|
|
86
|
+
See individual guides for suggested prompts and supported tools and resources:
|
|
87
|
+
|
|
88
|
+
- [Insight Hub](./insight-hub/README.md)\
|
|
89
|
+
Get your top events and invite your LLM to help you fix them.
|
|
90
|
+
- [Reflect](./reflect/README.md)
|
|
91
|
+
- [API Hub](./api-hub/README.md)
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// Tool definitions for API Hub API client
|
|
3
|
+
export class ApiHubClient {
|
|
4
|
+
headers;
|
|
5
|
+
constructor(token) {
|
|
6
|
+
this.headers = {
|
|
7
|
+
"Authorization": `Bearer ${token}`,
|
|
8
|
+
"Content-Type": "application/json",
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
async getPortals() {
|
|
12
|
+
const response = await fetch("https://api.portal.swaggerhub.com/v1/portals", {
|
|
13
|
+
method: "GET",
|
|
14
|
+
headers: this.headers,
|
|
15
|
+
});
|
|
16
|
+
return response.json();
|
|
17
|
+
}
|
|
18
|
+
async createPortal(body) {
|
|
19
|
+
const response = await fetch(`https://api.portal.swaggerhub.com/v1/portals`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: this.headers,
|
|
22
|
+
body: JSON.stringify(body),
|
|
23
|
+
});
|
|
24
|
+
return response.json();
|
|
25
|
+
}
|
|
26
|
+
async getPortal(portalId) {
|
|
27
|
+
const response = await fetch(`https://api.portal.swaggerhub.com/v1/portals/${portalId}`, {
|
|
28
|
+
method: "GET",
|
|
29
|
+
headers: this.headers,
|
|
30
|
+
});
|
|
31
|
+
return response.json();
|
|
32
|
+
}
|
|
33
|
+
async deletePortal(portalId) {
|
|
34
|
+
await fetch(`https://api.portal.swaggerhub.com/v1/portals/${portalId}`, {
|
|
35
|
+
method: "DELETE",
|
|
36
|
+
headers: this.headers,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async updatePortal(portalId, body) {
|
|
40
|
+
const response = await fetch(`https://api.portal.swaggerhub.com/v1/portals/${portalId}`, {
|
|
41
|
+
method: "PATCH",
|
|
42
|
+
headers: this.headers,
|
|
43
|
+
body: JSON.stringify(body),
|
|
44
|
+
});
|
|
45
|
+
return response.json();
|
|
46
|
+
}
|
|
47
|
+
async getPortalProducts(portalId) {
|
|
48
|
+
const response = await fetch(`https://api.portal.swaggerhub.com/v1/portals/${portalId}/products`, {
|
|
49
|
+
method: "GET",
|
|
50
|
+
headers: this.headers,
|
|
51
|
+
});
|
|
52
|
+
return response.json();
|
|
53
|
+
}
|
|
54
|
+
async createPortalProduct(portalId, body) {
|
|
55
|
+
const response = await fetch(`https://api.portal.swaggerhub.com/v1/portals/${portalId}/products`, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: this.headers,
|
|
58
|
+
body: JSON.stringify(body),
|
|
59
|
+
});
|
|
60
|
+
return response.json();
|
|
61
|
+
}
|
|
62
|
+
async getPortalProduct(productId) {
|
|
63
|
+
const response = await fetch(`https://api.portal.swaggerhub.com/v1/products/${productId}`, {
|
|
64
|
+
method: "GET",
|
|
65
|
+
headers: this.headers,
|
|
66
|
+
});
|
|
67
|
+
return response.json();
|
|
68
|
+
}
|
|
69
|
+
async deletePortalProduct(productId) {
|
|
70
|
+
await fetch(`https://api.portal.swaggerhub.com/v1/products/${productId}`, {
|
|
71
|
+
method: "DELETE",
|
|
72
|
+
headers: this.headers,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async updatePortalProduct(productId, body) {
|
|
76
|
+
const response = await fetch(`https://api.portal.swaggerhub.com/v1/products/${productId}`, {
|
|
77
|
+
method: "PATCH",
|
|
78
|
+
headers: this.headers,
|
|
79
|
+
body: JSON.stringify(body),
|
|
80
|
+
});
|
|
81
|
+
return response.json();
|
|
82
|
+
}
|
|
83
|
+
registerTools(server) {
|
|
84
|
+
server.tool("list_portals", "Search for available portals within API Hub. Only portals where you have at least a designer role, either at the product level or organization level, are returned.", {}, async (_args, _extra) => {
|
|
85
|
+
const response = await this.getPortals();
|
|
86
|
+
return {
|
|
87
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
server.tool("create_portal", "Create a new portal within API Hub.", {
|
|
91
|
+
name: z.string().optional().describe("The portal name."),
|
|
92
|
+
subdomain: z.string().describe("The portal subdomain."),
|
|
93
|
+
offline: z.boolean().optional().describe("If set to true the portal will not be visible to customers."),
|
|
94
|
+
routing: z.string().optional().describe("Determines the routing strategy ('browser' or 'proxy')."),
|
|
95
|
+
credentialsEnabled: z.string().optional().describe("Indicates if credentials are enabled for the portal."),
|
|
96
|
+
swaggerHubOrganizationId: z.string().describe("The corresponding API Hub (formerly SwaggerHub) organization UUID."),
|
|
97
|
+
openapiRenderer: z.string().optional().describe("Portal level setting for the OpenAPI renderer. SWAGGER_UI - Use the Swagger UI renderer. ELEMENTS - Use the Elements renderer. TOGGLE - Switch between the two renderers with elements set as the default."),
|
|
98
|
+
pageContentFormat: z.string().optional().describe("The format of the page content.")
|
|
99
|
+
}, async (args, _extra) => {
|
|
100
|
+
const response = await this.createPortal(args);
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
server.tool("get_portal", "Retrieve information about a specific portal.", { portalId: z.string().describe("Portal UUID or subdomain.") }, async (args, _extra) => {
|
|
106
|
+
const response = await this.getPortal(args.portalId);
|
|
107
|
+
return {
|
|
108
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
server.tool("delete_portal", "Delete a portal.", { portalId: z.string().describe("Portal UUID or subdomain.") }, async (args, _extra) => {
|
|
112
|
+
await this.deletePortal(args.portalId);
|
|
113
|
+
return {
|
|
114
|
+
content: [{ type: "text", text: "Portal deleted successfully." }],
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
server.tool("update_portal", "Update a specifc portal's configuration", {
|
|
118
|
+
portalId: z.string().describe("Portal UUID or subdomain."),
|
|
119
|
+
name: z.string().optional().describe("The portal name."),
|
|
120
|
+
subdomain: z.string().optional().describe("The portal subdomain."),
|
|
121
|
+
customDomain: z.boolean().optional().describe("Indicates if the portal has a custom domain."),
|
|
122
|
+
gtmKey: z.string().optional().describe("Google Tag Manager key for the portal."),
|
|
123
|
+
offline: z.boolean().optional().describe("If set to true the portal will not be visible to customers."),
|
|
124
|
+
routing: z.string().optional().describe("Determines the routing strategy ('browser' or 'proxy')."),
|
|
125
|
+
credentialsEnabled: z.boolean().optional().describe("Indicates if credentials are enabled for the portal."),
|
|
126
|
+
openapiRenderer: z.string().optional().describe("Portal level setting for the OpenAPI renderer. SWAGGER_UI - Use the Swagger UI renderer. ELEMENTS - Use the Elements renderer. TOGGLE - Switch between the two renderers with elements set as the default."),
|
|
127
|
+
pageContentFormat: z.string().optional().describe("The format of the page content.")
|
|
128
|
+
}, async (args, _extra) => {
|
|
129
|
+
const response = await this.updatePortal(args.portalId, args);
|
|
130
|
+
return {
|
|
131
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
server.tool("list_portal_products", "Get products for a specific portal that match your criteria.", { portalId: z.string().describe("Portal UUID or subdomain.") }, async (args, _extra) => {
|
|
135
|
+
const response = await this.getPortalProducts(args.portalId);
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
server.tool("create_portal_product", "Create a new product for a specific portal.", {
|
|
141
|
+
portalId: z.string().describe("Portal UUID or subdomain."),
|
|
142
|
+
type: z.string().describe("Product type (Allowed values: 'new', 'copy')."),
|
|
143
|
+
name: z.string().describe("Product name."),
|
|
144
|
+
slug: z.string().describe("URL component for this product. Must be unique within the portal."),
|
|
145
|
+
description: z.string().optional().describe("Product description."),
|
|
146
|
+
public: z.boolean().optional().describe("Indicates if the product is public."),
|
|
147
|
+
hidden: z.string().optional().describe("Indicates if the product is hidden."),
|
|
148
|
+
role: z.boolean().optional().describe("Indicates if the product has a role.")
|
|
149
|
+
}, async (args, _extra) => {
|
|
150
|
+
const response = await this.createPortalProduct(args.portalId, args);
|
|
151
|
+
return {
|
|
152
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
server.tool("get_portal_product", "Retrieve information about a specific product resource.", { productId: z.string().describe("Product UUID, or identifier in the format.") }, async (args, _extra) => {
|
|
156
|
+
const response = await this.getPortalProduct(args.productId);
|
|
157
|
+
return {
|
|
158
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
server.tool("delete_portal_product", "Delete a product from a specific portal", { productId: z.string().describe("Product UUID, or identifier in the format.") }, async (args, _extra) => {
|
|
162
|
+
await this.deletePortalProduct(args.productId);
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: "text", text: "Product deleted successfully." }],
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
server.tool("update_portal_product", "Update a product's settings within a specific portal.", {
|
|
168
|
+
productId: z.string().describe("Product UUID, or identifier in the format."),
|
|
169
|
+
name: z.string().optional().describe("Product name."),
|
|
170
|
+
slug: z.string().optional().describe("URL component for this product. Must be unique within the portal."),
|
|
171
|
+
description: z.string().optional().describe("Product description."),
|
|
172
|
+
public: z.boolean().optional().describe("Indicates if the product is public."),
|
|
173
|
+
hidden: z.string().optional().describe("Indicates if the product is hidden.")
|
|
174
|
+
}, async (args, _extra) => {
|
|
175
|
+
const response = await this.updatePortalProduct(args.productId, args);
|
|
176
|
+
return {
|
|
177
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import Bugsnag from "./common/bugsnag.js";
|
|
5
|
+
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "./common/info.js";
|
|
6
|
+
import { InsightHubClient } from "./insight-hub/client.js";
|
|
7
|
+
import { ReflectClient } from "./reflect/client.js";
|
|
8
|
+
import { ApiHubClient } from "./api-hub/client.js";
|
|
9
|
+
// This is used to report errors in the MCP server itself
|
|
10
|
+
// If you want to use your own BugSnag API key, set the MCP_SERVER_INSIGHT_HUB_API_KEY environment variable
|
|
11
|
+
const McpServerBugsnagAPIKey = process.env.MCP_SERVER_INSIGHT_HUB_API_KEY;
|
|
12
|
+
if (McpServerBugsnagAPIKey) {
|
|
13
|
+
Bugsnag.start(McpServerBugsnagAPIKey);
|
|
14
|
+
}
|
|
15
|
+
async function main() {
|
|
16
|
+
const server = new McpServer({
|
|
17
|
+
name: MCP_SERVER_NAME,
|
|
18
|
+
version: MCP_SERVER_VERSION,
|
|
19
|
+
}, {
|
|
20
|
+
capabilities: {
|
|
21
|
+
resources: { listChanged: true }, // Server supports dynamic resource lists
|
|
22
|
+
tools: { listChanged: true }, // Server supports dynamic tool lists
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
const reflectToken = process.env.REFLECT_API_TOKEN;
|
|
26
|
+
const insightHubToken = process.env.INSIGHT_HUB_AUTH_TOKEN;
|
|
27
|
+
const apiHubToken = process.env.API_HUB_API_KEY;
|
|
28
|
+
if (!reflectToken && !insightHubToken && !apiHubToken) {
|
|
29
|
+
console.error("Please set one of REFLECT_API_TOKEN, INSIGHT_HUB_AUTH_TOKEN or API_HUB_API_KEY environment variables");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
if (reflectToken) {
|
|
33
|
+
const reflectClient = new ReflectClient(reflectToken);
|
|
34
|
+
reflectClient.registerTools(server);
|
|
35
|
+
reflectClient.registerResources(server);
|
|
36
|
+
}
|
|
37
|
+
if (insightHubToken) {
|
|
38
|
+
const insightHubClient = new InsightHubClient(insightHubToken);
|
|
39
|
+
insightHubClient.registerTools(server);
|
|
40
|
+
insightHubClient.registerResources(server);
|
|
41
|
+
}
|
|
42
|
+
if (apiHubToken) {
|
|
43
|
+
const apiHubClient = new ApiHubClient(apiHubToken);
|
|
44
|
+
apiHubClient.registerTools(server);
|
|
45
|
+
}
|
|
46
|
+
const transport = new StdioServerTransport();
|
|
47
|
+
await server.connect(transport);
|
|
48
|
+
}
|
|
49
|
+
main().catch((error) => {
|
|
50
|
+
console.error("Fatal error in main():", error);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { BaseAPI, pickFieldsFromArray } from './base.js';
|
|
2
|
+
// --- API Class ---
|
|
3
|
+
export class CurrentUserAPI extends BaseAPI {
|
|
4
|
+
static organizationFields = ['id', 'name'];
|
|
5
|
+
static projectFields = ['id', 'name', 'slug'];
|
|
6
|
+
constructor(configuration) {
|
|
7
|
+
super(configuration);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* List the current user's organizations
|
|
11
|
+
* GET /user/organizations
|
|
12
|
+
*/
|
|
13
|
+
async listUserOrganizations(options = {}) {
|
|
14
|
+
const { admin, paginate = false, ...queryOptions } = options;
|
|
15
|
+
const params = new URLSearchParams();
|
|
16
|
+
if (admin !== undefined)
|
|
17
|
+
params.append('admin', String(admin));
|
|
18
|
+
for (const [key, value] of Object.entries(queryOptions)) {
|
|
19
|
+
if (value !== undefined)
|
|
20
|
+
params.append(key, String(value));
|
|
21
|
+
}
|
|
22
|
+
const url = params.toString() ? `/user/organizations?${params}` : '/user/organizations';
|
|
23
|
+
const data = await this.request({
|
|
24
|
+
method: 'GET',
|
|
25
|
+
url,
|
|
26
|
+
}, paginate);
|
|
27
|
+
// Only return allowed fields
|
|
28
|
+
return pickFieldsFromArray(data, CurrentUserAPI.organizationFields);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* List projects for a given organization
|
|
32
|
+
* GET /organizations/{organization_id}/projects
|
|
33
|
+
* @param organizationId The organization ID
|
|
34
|
+
* @param options Optional parameters for filtering, pagination, etc.
|
|
35
|
+
* @returns A promise that resolves to the list of projects in the organization
|
|
36
|
+
*/
|
|
37
|
+
async getOrganizationProjects(organizationId, options = {}) {
|
|
38
|
+
const { paginate = false, ...queryOptions } = options;
|
|
39
|
+
const params = new URLSearchParams();
|
|
40
|
+
for (const [key, value] of Object.entries(queryOptions)) {
|
|
41
|
+
if (value !== undefined)
|
|
42
|
+
params.append(key, String(value));
|
|
43
|
+
}
|
|
44
|
+
const url = params.toString()
|
|
45
|
+
? `/organizations/${organizationId}/projects?${params}`
|
|
46
|
+
: `/organizations/${organizationId}/projects`;
|
|
47
|
+
const data = await this.request({
|
|
48
|
+
method: 'GET',
|
|
49
|
+
url,
|
|
50
|
+
}, paginate);
|
|
51
|
+
// Only return allowed fields
|
|
52
|
+
return pickFieldsFromArray(data, CurrentUserAPI.projectFields);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { BaseAPI } from './base.js';
|
|
2
|
+
// --- API Class ---
|
|
3
|
+
export class ErrorAPI extends BaseAPI {
|
|
4
|
+
constructor(configuration) {
|
|
5
|
+
super(configuration);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* View an Error on a Project
|
|
9
|
+
* GET /projects/{project_id}/errors/{error_id}
|
|
10
|
+
*/
|
|
11
|
+
async viewErrorOnProject(projectId, errorId, options = {}) {
|
|
12
|
+
const params = new URLSearchParams();
|
|
13
|
+
for (const [key, value] of Object.entries(options)) {
|
|
14
|
+
if (value !== undefined)
|
|
15
|
+
params.append(key, String(value));
|
|
16
|
+
}
|
|
17
|
+
const url = params.toString()
|
|
18
|
+
? `/projects/${projectId}/errors/${errorId}?${params}`
|
|
19
|
+
: `/projects/${projectId}/errors/${errorId}`;
|
|
20
|
+
return (await this.request({
|
|
21
|
+
method: 'GET',
|
|
22
|
+
url,
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* View the latest Event on an Error
|
|
27
|
+
* GET /errors/{error_id}/latest_event
|
|
28
|
+
*/
|
|
29
|
+
async viewLatestEventOnError(errorId, options = {}) {
|
|
30
|
+
const params = new URLSearchParams();
|
|
31
|
+
for (const [key, value] of Object.entries(options)) {
|
|
32
|
+
if (value !== undefined)
|
|
33
|
+
params.append(key, String(value));
|
|
34
|
+
}
|
|
35
|
+
const url = params.toString()
|
|
36
|
+
? `/errors/${errorId}/latest_event?${params}`
|
|
37
|
+
: `/errors/${errorId}/latest_event`;
|
|
38
|
+
return (await this.request({
|
|
39
|
+
method: 'GET',
|
|
40
|
+
url,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* View an Event by ID
|
|
45
|
+
* GET /projects/{project_id}/events/{event_id}
|
|
46
|
+
*/
|
|
47
|
+
async viewEventById(projectId, eventId, options = {}) {
|
|
48
|
+
const params = new URLSearchParams();
|
|
49
|
+
for (const [key, value] of Object.entries(options)) {
|
|
50
|
+
if (value !== undefined)
|
|
51
|
+
params.append(key, String(value));
|
|
52
|
+
}
|
|
53
|
+
const url = params.toString()
|
|
54
|
+
? `/projects/${projectId}/events/${eventId}?${params}`
|
|
55
|
+
: `/projects/${projectId}/events/${eventId}`;
|
|
56
|
+
return (await this.request({
|
|
57
|
+
method: 'GET',
|
|
58
|
+
url,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Utility to pick only allowed fields from an object
|
|
2
|
+
export function pickFields(obj, keys) {
|
|
3
|
+
const result = {};
|
|
4
|
+
for (const key of keys) {
|
|
5
|
+
if (key in obj) {
|
|
6
|
+
result[key] = obj[key];
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
// Utility to pick only allowed fields from an array of objects
|
|
12
|
+
export function pickFieldsFromArray(arr, keys) {
|
|
13
|
+
return arr.map(obj => pickFields(obj, keys));
|
|
14
|
+
}
|
|
15
|
+
export class BaseAPI {
|
|
16
|
+
configuration;
|
|
17
|
+
constructor(configuration) {
|
|
18
|
+
this.configuration = configuration;
|
|
19
|
+
}
|
|
20
|
+
async request(options, paginate = false) {
|
|
21
|
+
const headers = {
|
|
22
|
+
...this.configuration.headers,
|
|
23
|
+
...options.headers,
|
|
24
|
+
};
|
|
25
|
+
headers['Authorization'] = `token ${this.configuration.authToken}`;
|
|
26
|
+
const fetchOptions = {
|
|
27
|
+
method: options.method,
|
|
28
|
+
headers,
|
|
29
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
30
|
+
};
|
|
31
|
+
const url = options.url.startsWith('http') ? options.url : `${this.configuration.basePath || ''}${options.url}`;
|
|
32
|
+
let results = [];
|
|
33
|
+
let nextUrl = url;
|
|
34
|
+
do {
|
|
35
|
+
const response = await fetch(nextUrl, fetchOptions);
|
|
36
|
+
const data = await response.json();
|
|
37
|
+
if (paginate) {
|
|
38
|
+
results = results.concat(data);
|
|
39
|
+
const link = response.headers.get('Link');
|
|
40
|
+
if (link) {
|
|
41
|
+
const match = link.match(/<([^>]+)>;\s*rel="next"/);
|
|
42
|
+
nextUrl = match ? match[1] : undefined;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
nextUrl = undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
} while (paginate && nextUrl);
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
|
|
3
|
+
import { CurrentUserAPI, ErrorAPI, Configuration } from "./client/index.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import Bugsnag from "../common/bugsnag.js";
|
|
6
|
+
export class InsightHubClient {
|
|
7
|
+
currentUserApi;
|
|
8
|
+
errorsApi;
|
|
9
|
+
constructor(token) {
|
|
10
|
+
const config = new Configuration({
|
|
11
|
+
authToken: token,
|
|
12
|
+
headers: {
|
|
13
|
+
"User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`,
|
|
14
|
+
"Content-Type": "application/json",
|
|
15
|
+
},
|
|
16
|
+
basePath: "https://api.bugsnag.com",
|
|
17
|
+
});
|
|
18
|
+
this.currentUserApi = new CurrentUserAPI(config);
|
|
19
|
+
this.errorsApi = new ErrorAPI(config);
|
|
20
|
+
}
|
|
21
|
+
async listOrgs() {
|
|
22
|
+
return this.currentUserApi.listUserOrganizations();
|
|
23
|
+
}
|
|
24
|
+
async listProjects(orgId, options) {
|
|
25
|
+
options = {
|
|
26
|
+
...options,
|
|
27
|
+
paginate: true
|
|
28
|
+
};
|
|
29
|
+
return this.currentUserApi.getOrganizationProjects(orgId, options);
|
|
30
|
+
}
|
|
31
|
+
async getErrorDetails(projectId, errorId) {
|
|
32
|
+
return this.errorsApi.viewErrorOnProject(projectId, errorId);
|
|
33
|
+
}
|
|
34
|
+
async getLatestErrorEvent(errorId) {
|
|
35
|
+
return this.errorsApi.viewLatestEventOnError(errorId);
|
|
36
|
+
}
|
|
37
|
+
async getProjectEvent(projectId, eventId) {
|
|
38
|
+
return this.errorsApi.viewEventById(projectId, eventId);
|
|
39
|
+
}
|
|
40
|
+
async findEventById(eventId) {
|
|
41
|
+
const projects = await this.listOrgs().then(([org]) => this.listProjects(org.id));
|
|
42
|
+
const eventDetails = await Promise.all(projects.map((p) => this.getProjectEvent(p.id, eventId).catch(_e => null)));
|
|
43
|
+
return eventDetails.find(event => !!event);
|
|
44
|
+
}
|
|
45
|
+
registerTools(server) {
|
|
46
|
+
server.tool("list_insight_hub_projects", "List all projects in an organization", { orgId: z.string().describe("ID of the organization to list projects for") }, async (args, _extra) => {
|
|
47
|
+
try {
|
|
48
|
+
if (!args.orgId)
|
|
49
|
+
throw new Error("orgId argument is required");
|
|
50
|
+
const response = await this.listProjects(args.orgId);
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
Bugsnag.notify(e);
|
|
57
|
+
throw e;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
server.tool("get_insight_hub_error", "Get error details from a project", {
|
|
61
|
+
projectId: z.string().describe("ID of the project"),
|
|
62
|
+
errorId: z.string().describe("ID of the error to fetch"),
|
|
63
|
+
}, async (args, _extra) => {
|
|
64
|
+
try {
|
|
65
|
+
if (!args.projectId || !args.errorId)
|
|
66
|
+
throw new Error("Both projectId and errorId arguments are required");
|
|
67
|
+
const response = await this.getErrorDetails(args.projectId, args.errorId);
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
Bugsnag.notify(e);
|
|
74
|
+
throw e;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
server.tool("get_insight_hub_error_latest_event", "Get the latest event for an error", {
|
|
78
|
+
errorId: z.string().describe("ID of the error to get the latest event for"),
|
|
79
|
+
}, async (args, _extra) => {
|
|
80
|
+
try {
|
|
81
|
+
if (!args.errorId)
|
|
82
|
+
throw new Error("errorId argument is required");
|
|
83
|
+
const response = await this.getLatestErrorEvent(args.errorId);
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
Bugsnag.notify(e);
|
|
90
|
+
throw e;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
server.tool("get_insight_hub_event_details", "Get details of a specific event on Insight Hub", {
|
|
94
|
+
link: z.string().describe("Link to the event details"),
|
|
95
|
+
}, async (args, _extra) => {
|
|
96
|
+
try {
|
|
97
|
+
if (!args.link)
|
|
98
|
+
throw new Error("link argument is required");
|
|
99
|
+
const url = new URL(args.link);
|
|
100
|
+
const eventId = url.searchParams.get("event_id");
|
|
101
|
+
const projectSlug = url.pathname.split('/')[2];
|
|
102
|
+
if (!projectSlug || !eventId)
|
|
103
|
+
throw new Error("Both projectSlug and eventId must be present in the link");
|
|
104
|
+
// get the project id from list of projects
|
|
105
|
+
// limitation: this assumes a single page of results, so will not work for orgs with >100 projects
|
|
106
|
+
const orgId = await this.currentUserApi.listUserOrganizations().then(orgs => orgs[0].id);
|
|
107
|
+
const projectId = await this.listProjects(orgId).then(projects => projects.find((p) => p.slug === projectSlug)?.id);
|
|
108
|
+
const response = await this.getProjectEvent(projectId, eventId);
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
Bugsnag.notify(e);
|
|
115
|
+
throw e;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
registerResources(server) {
|
|
120
|
+
server.resource("insight_hub_orgs", "insighthub://orgs", { description: "List all organizations in Insight Hub", mimeType: "application/json" }, async (uri) => {
|
|
121
|
+
try {
|
|
122
|
+
return {
|
|
123
|
+
contents: [{
|
|
124
|
+
uri: uri.href,
|
|
125
|
+
text: JSON.stringify(await this.listOrgs())
|
|
126
|
+
}]
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
Bugsnag.notify(e);
|
|
131
|
+
throw e;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
server.resource("insight_hub_event", new ResourceTemplate("insighthub://event/{id}", { list: undefined }), async (uri, { id }) => {
|
|
135
|
+
try {
|
|
136
|
+
return {
|
|
137
|
+
contents: [{
|
|
138
|
+
uri: uri.href,
|
|
139
|
+
text: JSON.stringify(await this.findEventById(id))
|
|
140
|
+
}]
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
Bugsnag.notify(e);
|
|
145
|
+
throw e;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@smartbear/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for interacting SmartBear Products",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-server-smartbear": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"config": {
|
|
14
|
+
"mcpServerName": "SmartBear MCP Server"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc && shx chmod +x dist/*.js",
|
|
18
|
+
"lint": "eslint . --ext .ts",
|
|
19
|
+
"prepare": "npm run build",
|
|
20
|
+
"watch": "tsc --watch"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@bugsnag/js": "^8.2.0",
|
|
24
|
+
"@modelcontextprotocol/sdk": "1.12.1"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@eslint/js": "^9.29.0",
|
|
28
|
+
"@types/node": "^22",
|
|
29
|
+
"eslint": "^9.29.0",
|
|
30
|
+
"globals": "^16.2.0",
|
|
31
|
+
"shx": "^0.3.4",
|
|
32
|
+
"typescript": "^5.6.2",
|
|
33
|
+
"typescript-eslint": "^8.34.1"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// ReflectClient class implementing the Client interface
|
|
3
|
+
export class ReflectClient {
|
|
4
|
+
headers;
|
|
5
|
+
constructor(token) {
|
|
6
|
+
this.headers = {
|
|
7
|
+
"X-API-KEY": `${token}`,
|
|
8
|
+
"Content-Type": "application/json",
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
async listReflectSuits() {
|
|
12
|
+
const response = await fetch("https://api.reflect.run/v1/suites", {
|
|
13
|
+
method: "GET",
|
|
14
|
+
headers: this.headers,
|
|
15
|
+
});
|
|
16
|
+
return response.json();
|
|
17
|
+
}
|
|
18
|
+
async listSuiteExecutions(suiteId) {
|
|
19
|
+
const response = await fetch(`https://api.reflect.run/v1/suites/${suiteId}/executions`, {
|
|
20
|
+
method: "GET",
|
|
21
|
+
headers: this.headers,
|
|
22
|
+
});
|
|
23
|
+
return response.json();
|
|
24
|
+
}
|
|
25
|
+
async getSuiteExecutionStatus(suiteId, executionId) {
|
|
26
|
+
const response = await fetch(`https://api.reflect.run/v1/suites/${suiteId}/executions/${executionId}`, {
|
|
27
|
+
method: "GET",
|
|
28
|
+
headers: this.headers,
|
|
29
|
+
});
|
|
30
|
+
return response.json();
|
|
31
|
+
}
|
|
32
|
+
async executeSuite(suiteId) {
|
|
33
|
+
const response = await fetch(`https://api.reflect.run/v1/suites/${suiteId}/executions`, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: this.headers,
|
|
36
|
+
});
|
|
37
|
+
return response.json();
|
|
38
|
+
}
|
|
39
|
+
async cancelSuiteExecution(suiteId, executionId) {
|
|
40
|
+
const response = await fetch(`https://api.reflect.run/v1/suites/${suiteId}/executions/${executionId}/cancel`, {
|
|
41
|
+
method: "PATCH",
|
|
42
|
+
headers: this.headers,
|
|
43
|
+
});
|
|
44
|
+
return response.json();
|
|
45
|
+
}
|
|
46
|
+
async listReflectTests() {
|
|
47
|
+
const response = await fetch("https://api.reflect.run/v1/tests", {
|
|
48
|
+
method: "GET",
|
|
49
|
+
headers: this.headers,
|
|
50
|
+
});
|
|
51
|
+
return response.json();
|
|
52
|
+
}
|
|
53
|
+
async runReflectTest(testId) {
|
|
54
|
+
const response = await fetch(`https://api.reflect.run/v1/tests/${testId}/executions`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: this.headers,
|
|
57
|
+
});
|
|
58
|
+
return response.json();
|
|
59
|
+
}
|
|
60
|
+
async getReflectTestStatus(testId, executionId) {
|
|
61
|
+
const response = await fetch(`https://api.reflect.run/v1/executions/${executionId}`, {
|
|
62
|
+
method: "GET",
|
|
63
|
+
headers: this.headers,
|
|
64
|
+
});
|
|
65
|
+
return response.json();
|
|
66
|
+
}
|
|
67
|
+
registerTools(server) {
|
|
68
|
+
server.tool("list_reflect_suites", "List all reflect suites", {}, async (_args, _extra) => {
|
|
69
|
+
const response = await this.listReflectSuits();
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
server.tool("list_reflect_suite_executions", "List all executions for a given reflect suite", { suiteId: z.string().describe("ID of the reflect suite to list executions for") }, async (args, _extra) => {
|
|
75
|
+
if (!args.suiteId)
|
|
76
|
+
throw new Error("suiteId argument is required");
|
|
77
|
+
const response = await this.listSuiteExecutions(args.suiteId);
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
server.tool("reflect_suite_execution_status", "Get the status of a reflect suite execution", {
|
|
83
|
+
suiteId: z.string().describe("ID of the reflect suite to list executions for"),
|
|
84
|
+
executionId: z.string().describe("ID of the reflect suite execution to get status for"),
|
|
85
|
+
}, async (args, _extra) => {
|
|
86
|
+
if (!args.suiteId || !args.executionId)
|
|
87
|
+
throw new Error("Both suiteId and executionId arguments are required");
|
|
88
|
+
const response = await this.getSuiteExecutionStatus(args.suiteId, args.executionId);
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
server.tool("reflect_suite_execution", "Execute a reflect suite", { suiteId: z.string().describe("ID of the reflect suite to list executions for") }, async (args, _extra) => {
|
|
94
|
+
if (!args.suiteId)
|
|
95
|
+
throw new Error("suiteId argument is required");
|
|
96
|
+
const response = await this.executeSuite(args.suiteId);
|
|
97
|
+
return {
|
|
98
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
server.tool("cancel_reflect_suite_execution", "Cancel a reflect suite execution", {
|
|
102
|
+
suiteId: z.string().describe("ID of the reflect suite to cancel execution for"),
|
|
103
|
+
executionId: z.string().describe("ID of the reflect suite execution to cancel"),
|
|
104
|
+
}, async (args, _extra) => {
|
|
105
|
+
if (!args.suiteId || !args.executionId)
|
|
106
|
+
throw new Error("Both suiteId and executionId arguments are required");
|
|
107
|
+
const response = await this.cancelSuiteExecution(args.suiteId, args.executionId);
|
|
108
|
+
return {
|
|
109
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
server.tool("list_reflect_tests", "List all reflect tests", {}, async (_args, _extra) => {
|
|
113
|
+
const response = await this.listReflectTests();
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
server.tool("run_reflect_test", "Run a reflect test", { testId: z.string().describe("ID of the reflect test to run") }, async (args, _extra) => {
|
|
119
|
+
if (!args.testId)
|
|
120
|
+
throw new Error("testId argument is required");
|
|
121
|
+
const response = await this.runReflectTest(args.testId);
|
|
122
|
+
return {
|
|
123
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
server.tool("reflect_test_status", "Get the status of a reflect test execution", {
|
|
127
|
+
testId: z.string().describe("ID of the reflect test to run"),
|
|
128
|
+
executionId: z.string().describe("ID of the reflect test execution to get status for"),
|
|
129
|
+
}, async (args, _extra) => {
|
|
130
|
+
if (!args.testId || !args.executionId)
|
|
131
|
+
throw new Error("Both testId and executionId arguments are required");
|
|
132
|
+
const response = await this.getReflectTestStatus(args.testId, args.executionId);
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
registerResources(_server) {
|
|
139
|
+
// Reflect does not currently support dynamic resources
|
|
140
|
+
}
|
|
141
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@smartbear/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for interacting SmartBear Products",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-server-smartbear": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"config": {
|
|
14
|
+
"mcpServerName": "SmartBear MCP Server"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc && shx chmod +x dist/*.js",
|
|
18
|
+
"lint": "eslint . --ext .ts",
|
|
19
|
+
"prepare": "npm run build",
|
|
20
|
+
"watch": "tsc --watch"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@bugsnag/js": "^8.2.0",
|
|
24
|
+
"@modelcontextprotocol/sdk": "1.12.1"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@eslint/js": "^9.29.0",
|
|
28
|
+
"@types/node": "^22",
|
|
29
|
+
"eslint": "^9.29.0",
|
|
30
|
+
"globals": "^16.2.0",
|
|
31
|
+
"shx": "^0.3.4",
|
|
32
|
+
"typescript": "^5.6.2",
|
|
33
|
+
"typescript-eslint": "^8.34.1"
|
|
34
|
+
}
|
|
35
|
+
}
|