@smartbear/mcp 0.25.0 → 0.25.1
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/dist/bearq/client.js +13 -12
- package/dist/bugsnag/client.js +44 -23
- package/dist/collaborator/client.js +19 -14
- package/dist/common/client-registry.js +20 -46
- package/dist/common/request-context.js +20 -0
- package/dist/common/server.js +1 -14
- package/dist/common/transport-http.js +63 -80
- package/dist/common/transport-stdio.js +24 -27
- package/dist/package.json.js +1 -1
- package/dist/pactflow/client.js +43 -30
- package/dist/qmetry/client.js +18 -20
- package/dist/qtm4j/client.js +31 -17
- package/dist/reflect/client.js +24 -15
- package/dist/reflect/config/constants.js +2 -0
- package/dist/swagger/client.js +28 -25
- package/dist/zephyr/client.js +21 -14
- package/package.json +1 -1
package/dist/bearq/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
|
|
3
|
+
import { getRequestHeader } from "../common/request-context.js";
|
|
3
4
|
import { ToolError } from "../common/tools.js";
|
|
4
5
|
import { DEFAULT_API_BASE_URL, AUTHORIZATION_HEADER } from "./config/constants.js";
|
|
5
6
|
import { ChatWithQaLead } from "./tool/tasks/chat-with-qa-lead.js";
|
|
@@ -15,33 +16,33 @@ import { RunTestsInFunctionalAreas } from "./tool/tasks/run-tests-in-functional-
|
|
|
15
16
|
import { StopTask } from "./tool/tasks/stop-task.js";
|
|
16
17
|
import { WaitForTask } from "./tool/tasks/wait-for-task.js";
|
|
17
18
|
const ConfigurationSchema = z.object({
|
|
19
|
+
api_token: z.string().describe("BearQ workspace API token (Bearer)."),
|
|
18
20
|
api_base_url: z.string().optional().describe(
|
|
19
21
|
"Override the BearQ public API base URL. Defaults to https://api.bearq.smartbear.com"
|
|
20
22
|
)
|
|
21
23
|
});
|
|
22
|
-
const AuthenticationSchema = z.object({
|
|
23
|
-
api_token: z.string().describe("BearQ workspace API token (Bearer).")
|
|
24
|
-
});
|
|
25
24
|
class BearQClient {
|
|
26
|
-
|
|
25
|
+
_apiToken;
|
|
27
26
|
_baseUrl = DEFAULT_API_BASE_URL;
|
|
28
27
|
name = "BearQ";
|
|
29
28
|
capabilityPrefix = "bearq";
|
|
30
29
|
configPrefix = "BearQ";
|
|
31
30
|
config = ConfigurationSchema;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
this.server = server;
|
|
31
|
+
async configure(_server, config) {
|
|
32
|
+
this._apiToken = config.api_token;
|
|
35
33
|
if (config.api_base_url) this._baseUrl = config.api_base_url;
|
|
36
34
|
}
|
|
37
35
|
getAuthToken() {
|
|
38
|
-
|
|
36
|
+
const contextHeader = getRequestHeader(AUTHORIZATION_HEADER);
|
|
37
|
+
if (contextHeader) {
|
|
38
|
+
let token = Array.isArray(contextHeader) ? contextHeader[0] : contextHeader;
|
|
39
|
+
if (token.startsWith("Bearer ")) token = token.substring(7);
|
|
40
|
+
return token;
|
|
41
|
+
}
|
|
42
|
+
return this._apiToken ?? null;
|
|
39
43
|
}
|
|
40
44
|
isConfigured() {
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
hasAuth() {
|
|
44
|
-
return this.isConfigured() && !!this.getAuthToken();
|
|
45
|
+
return true;
|
|
45
46
|
}
|
|
46
47
|
getBaseUrl() {
|
|
47
48
|
return this._baseUrl;
|
package/dist/bugsnag/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
|
|
3
|
+
import { getRequestHeader } from "../common/request-context.js";
|
|
3
4
|
import { ToolError } from "../common/tools.js";
|
|
4
5
|
import { CurrentUserAPI } from "./client/api/CurrentUser.js";
|
|
5
6
|
import { Configuration } from "./client/api/configuration.js";
|
|
@@ -39,14 +40,11 @@ const EXCLUDED_EVENT_FIELDS = /* @__PURE__ */ new Set([
|
|
|
39
40
|
// This is searches multiple fields and is more a convenience for humans, we're removing to avoid over-matching
|
|
40
41
|
]);
|
|
41
42
|
const ConfigurationSchema = z.object({
|
|
43
|
+
auth_token: z.string().describe("BugSnag personal access token"),
|
|
42
44
|
project_api_key: z.string().describe("BugSnag project API key").optional(),
|
|
43
|
-
endpoint: z.url().describe("BugSnag endpoint URL").optional()
|
|
44
|
-
});
|
|
45
|
-
const AuthenticationSchema = z.object({
|
|
46
|
-
auth_token: z.string().describe("BugSnag personal access token").optional()
|
|
45
|
+
endpoint: z.string().url().describe("BugSnag endpoint URL").optional()
|
|
47
46
|
});
|
|
48
47
|
class BugsnagClient {
|
|
49
|
-
server;
|
|
50
48
|
cache;
|
|
51
49
|
_projectApiKey;
|
|
52
50
|
_isConfigured = false;
|
|
@@ -54,7 +52,7 @@ class BugsnagClient {
|
|
|
54
52
|
_errorsApi;
|
|
55
53
|
_projectApi;
|
|
56
54
|
_appEndpoint;
|
|
57
|
-
|
|
55
|
+
_authToken;
|
|
58
56
|
get currentUserApi() {
|
|
59
57
|
if (!this._currentUserApi) throw new Error("Client not configured");
|
|
60
58
|
return this._currentUserApi;
|
|
@@ -76,9 +74,7 @@ class BugsnagClient {
|
|
|
76
74
|
configPrefix = "Bugsnag";
|
|
77
75
|
config = ConfigurationSchema;
|
|
78
76
|
defaultToolsets = ["Projects"];
|
|
79
|
-
authenticationFields = AuthenticationSchema;
|
|
80
77
|
async configure(server, config) {
|
|
81
|
-
this.server = server;
|
|
82
78
|
this.cache = server.getCache();
|
|
83
79
|
this._appEndpoint = this.getEndpoint(
|
|
84
80
|
"app",
|
|
@@ -86,17 +82,45 @@ class BugsnagClient {
|
|
|
86
82
|
config.endpoint
|
|
87
83
|
);
|
|
88
84
|
this._projectApiKey = config.project_api_key;
|
|
89
|
-
this.
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
this._authToken = config.auth_token;
|
|
86
|
+
await this.initializeApis(config);
|
|
87
|
+
}
|
|
88
|
+
getAuthToken() {
|
|
89
|
+
const contextHeader = getRequestHeader("Bugsnag-Auth-Token");
|
|
90
|
+
if (contextHeader) {
|
|
91
|
+
let token = Array.isArray(contextHeader) ? contextHeader[0] : contextHeader;
|
|
92
|
+
if (token.startsWith("token ")) {
|
|
93
|
+
token = token.substring(6);
|
|
94
|
+
}
|
|
95
|
+
return `token ${token}`;
|
|
96
|
+
}
|
|
97
|
+
const bearerToken = this.getBearerToken();
|
|
98
|
+
if (bearerToken) {
|
|
99
|
+
return bearerToken;
|
|
100
|
+
}
|
|
101
|
+
return this._authToken ? `token ${this._authToken}` : null;
|
|
102
|
+
}
|
|
103
|
+
getBearerToken() {
|
|
104
|
+
const contextHeader = getRequestHeader("Authorization");
|
|
105
|
+
if (contextHeader) {
|
|
106
|
+
let token = Array.isArray(contextHeader) ? contextHeader[0] : contextHeader;
|
|
107
|
+
if (token.startsWith("Bearer ")) {
|
|
108
|
+
token = token.substring(7);
|
|
109
|
+
}
|
|
110
|
+
return `Bearer ${token}`;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
async initializeApis(config) {
|
|
115
|
+
const apiConfig = new Configuration({
|
|
116
|
+
apiKey: (_name) => {
|
|
117
|
+
const authToken = this.getAuthToken();
|
|
92
118
|
if (authToken) {
|
|
93
|
-
return
|
|
119
|
+
return authToken;
|
|
94
120
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
return void 0;
|
|
121
|
+
throw new Error(
|
|
122
|
+
"Authentication token not found in request headers or configuration"
|
|
123
|
+
);
|
|
100
124
|
},
|
|
101
125
|
headers: {
|
|
102
126
|
"User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`,
|
|
@@ -110,17 +134,14 @@ class BugsnagClient {
|
|
|
110
134
|
config.endpoint
|
|
111
135
|
)
|
|
112
136
|
});
|
|
113
|
-
this._currentUserApi = new CurrentUserAPI(
|
|
114
|
-
this._errorsApi = new ErrorAPI(
|
|
115
|
-
this._projectApi = new ProjectAPI(
|
|
137
|
+
this._currentUserApi = new CurrentUserAPI(apiConfig);
|
|
138
|
+
this._errorsApi = new ErrorAPI(apiConfig);
|
|
139
|
+
this._projectApi = new ProjectAPI(apiConfig);
|
|
116
140
|
this._isConfigured = true;
|
|
117
141
|
}
|
|
118
142
|
isConfigured() {
|
|
119
143
|
return this._isConfigured;
|
|
120
144
|
}
|
|
121
|
-
hasAuth() {
|
|
122
|
-
return this.isConfigured() && !!this.apiConfig?.apiKey();
|
|
123
|
-
}
|
|
124
145
|
// If the endpoint is not provided, it will use the default API endpoint based on the project API key.
|
|
125
146
|
// if the project api key is not provided, the endpoint will be the default API endpoint.
|
|
126
147
|
// if the endpoint is provided, it will be used as is for custom domains, or normalized for known domains.
|
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { getRequestHeader } from "../common/request-context.js";
|
|
2
3
|
const ConfigurationSchema = z.object({
|
|
3
|
-
base_url: z.url().describe("Collaborator server base URL")
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
login: z.string().describe("Collaborator username for authentication").optional(),
|
|
7
|
-
ticket: z.string().describe("Collaborator login ticket for authentication").optional()
|
|
4
|
+
base_url: z.url().describe("Collaborator server base URL"),
|
|
5
|
+
username: z.string().describe("Collaborator username for authentication").optional(),
|
|
6
|
+
login_ticket: z.string().describe("Collaborator login ticket for authentication").optional()
|
|
8
7
|
});
|
|
9
8
|
class CollaboratorClient {
|
|
10
9
|
name = "Collaborator";
|
|
11
10
|
capabilityPrefix = "collaborator";
|
|
12
11
|
configPrefix = "Collaborator";
|
|
13
12
|
config = ConfigurationSchema;
|
|
14
|
-
authenticationFields = AuthenticationSchema;
|
|
15
13
|
baseUrl;
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
username;
|
|
15
|
+
loginTicket;
|
|
16
|
+
async configure(_server, config, _cache) {
|
|
18
17
|
this.baseUrl = config.base_url;
|
|
19
|
-
this.
|
|
18
|
+
this.username = config.username;
|
|
19
|
+
this.loginTicket = config.login_ticket;
|
|
20
20
|
}
|
|
21
21
|
isConfigured() {
|
|
22
22
|
return this.baseUrl !== void 0;
|
|
23
23
|
}
|
|
24
|
-
hasAuth() {
|
|
25
|
-
return this.isConfigured() && this.server?.getEnv("Login", this) !== void 0 && this.server?.getEnv("Ticket", this) !== void 0;
|
|
26
|
-
}
|
|
27
24
|
/**
|
|
28
25
|
* Calls the Collaborator API with the given commands, prepending authentication automatically.
|
|
29
26
|
* @param commands Array of Collaborator API commands (excluding authentication)
|
|
@@ -31,8 +28,16 @@ class CollaboratorClient {
|
|
|
31
28
|
*/
|
|
32
29
|
async call(commands) {
|
|
33
30
|
const url = `${this.baseUrl}/services/json/v1`;
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
let login = this.username;
|
|
32
|
+
let ticket = this.loginTicket;
|
|
33
|
+
const contextLogin = getRequestHeader("Collaborator-Login");
|
|
34
|
+
const contextTicket = getRequestHeader("Collaborator-Ticket");
|
|
35
|
+
if (contextLogin) {
|
|
36
|
+
login = Array.isArray(contextLogin) ? contextLogin[0] : contextLogin;
|
|
37
|
+
}
|
|
38
|
+
if (contextTicket) {
|
|
39
|
+
ticket = Array.isArray(contextTicket) ? contextTicket[0] : contextTicket;
|
|
40
|
+
}
|
|
36
41
|
const body = [
|
|
37
42
|
{
|
|
38
43
|
command: "SessionService.authenticate",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ZodURL
|
|
1
|
+
import { ZodURL } from "zod";
|
|
2
2
|
import { fullyUnwrapZodType, isOptionalType } from "./zod-utils.js";
|
|
3
3
|
class ClientRegistry {
|
|
4
4
|
entries = [];
|
|
@@ -84,57 +84,31 @@ class ClientRegistry {
|
|
|
84
84
|
return this.entries.filter((entry) => this.isClientEnabled(entry.name));
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
87
|
-
*
|
|
87
|
+
* Configures all enabled clients on the given MCP server
|
|
88
88
|
* @param server The MCP server on which the client is registered
|
|
89
89
|
* @param getConfigValue A function that obtains a configuration value for the given client and requirement name
|
|
90
|
-
* @returns The number of clients successfully
|
|
90
|
+
* @returns The number of clients successfully configured
|
|
91
91
|
*/
|
|
92
|
-
async
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const value = getConfigValue(configKey, client);
|
|
104
|
-
if (value) {
|
|
105
|
-
this.validateAllowedEndpoint(client.config.shape[configKey], value);
|
|
106
|
-
config[configKey] = value;
|
|
107
|
-
} else if (!isOptionalType(client.config.shape[configKey])) {
|
|
108
|
-
continue clientLoop;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
let parsedConfig;
|
|
112
|
-
try {
|
|
113
|
-
parsedConfig = client.config.parse(config);
|
|
114
|
-
} catch (error) {
|
|
115
|
-
if (error instanceof ZodError) {
|
|
116
|
-
console.warn(
|
|
117
|
-
`Configuration for client ${client.name} is invalid: ${error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ")}`
|
|
118
|
-
);
|
|
119
|
-
} else {
|
|
120
|
-
console.warn(
|
|
121
|
-
`Unable to apply configuration for client ${client.name}: ${error}`
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
await client.configure(server, parsedConfig);
|
|
127
|
-
if (!client.isConfigured()) {
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
if (authorizationCheck && !client.hasAuth()) {
|
|
131
|
-
continue;
|
|
92
|
+
async configure(server, getConfigValue, ignoreMissingRequiredConfigs = false) {
|
|
93
|
+
let configuredCount = 0;
|
|
94
|
+
entryLoop: for (const entry of this.getAll()) {
|
|
95
|
+
const config = {};
|
|
96
|
+
for (const configKey of Object.keys(entry.config.shape)) {
|
|
97
|
+
const value = getConfigValue(entry, configKey);
|
|
98
|
+
if (value !== null) {
|
|
99
|
+
this.validateAllowedEndpoint(entry.config.shape[configKey], value);
|
|
100
|
+
config[configKey] = value;
|
|
101
|
+
} else if (!ignoreMissingRequiredConfigs && !isOptionalType(entry.config.shape[configKey])) {
|
|
102
|
+
continue entryLoop;
|
|
132
103
|
}
|
|
133
104
|
}
|
|
134
|
-
await
|
|
135
|
-
|
|
105
|
+
await entry.configure(server, config);
|
|
106
|
+
if (entry.isConfigured()) {
|
|
107
|
+
await server.addClient(entry);
|
|
108
|
+
configuredCount++;
|
|
109
|
+
}
|
|
136
110
|
}
|
|
137
|
-
return
|
|
111
|
+
return configuredCount;
|
|
138
112
|
}
|
|
139
113
|
/**
|
|
140
114
|
* Clear all registrations (useful for testing)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
const requestContextStorage = new AsyncLocalStorage();
|
|
3
|
+
function withRequestContext(req, fn) {
|
|
4
|
+
return requestContextStorage.run({ headers: req.headers }, fn);
|
|
5
|
+
}
|
|
6
|
+
function getRequestContext() {
|
|
7
|
+
return requestContextStorage.getStore();
|
|
8
|
+
}
|
|
9
|
+
function getRequestHeader(name) {
|
|
10
|
+
const context = getRequestContext();
|
|
11
|
+
if (!context?.headers) return void 0;
|
|
12
|
+
const headerValue = context.headers[name] || context.headers[name.toLowerCase()];
|
|
13
|
+
return headerValue;
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
getRequestContext,
|
|
17
|
+
getRequestHeader,
|
|
18
|
+
requestContextStorage,
|
|
19
|
+
withRequestContext
|
|
20
|
+
};
|
package/dist/common/server.js
CHANGED
|
@@ -12,8 +12,7 @@ class SmartBearMcpServer extends McpServer {
|
|
|
12
12
|
elicitationSupported = false;
|
|
13
13
|
clients = [];
|
|
14
14
|
enabledToolsets;
|
|
15
|
-
|
|
16
|
-
constructor(getEnvFn, enabledToolsets) {
|
|
15
|
+
constructor(enabledToolsets) {
|
|
17
16
|
super(
|
|
18
17
|
{
|
|
19
18
|
name: MCP_SERVER_NAME,
|
|
@@ -29,7 +28,6 @@ class SmartBearMcpServer extends McpServer {
|
|
|
29
28
|
}
|
|
30
29
|
}
|
|
31
30
|
);
|
|
32
|
-
this.getEnvFn = getEnvFn;
|
|
33
31
|
this.cache = new CacheService();
|
|
34
32
|
if (enabledToolsets) {
|
|
35
33
|
this.enabledToolsets = enabledToolsets.split(",").map((s) => s.trim().toLowerCase());
|
|
@@ -38,17 +36,6 @@ class SmartBearMcpServer extends McpServer {
|
|
|
38
36
|
getCache() {
|
|
39
37
|
return this.cache;
|
|
40
38
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Makes the server's getEnv function available to clients, validating that it is defined in the client's authentication fields if a client is provided
|
|
43
|
-
*/
|
|
44
|
-
getEnv = (key, client) => {
|
|
45
|
-
if (client && !Object.keys(client.authenticationFields.shape).includes(key)) {
|
|
46
|
-
throw new Error(
|
|
47
|
-
`Environment variable "${key}" is not defined in the ${client.name} client's authentication schema.`
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
return this.getEnvFn(key, client);
|
|
51
|
-
};
|
|
52
39
|
setSamplingSupported(supported) {
|
|
53
40
|
this.samplingSupported = supported;
|
|
54
41
|
}
|
|
@@ -5,12 +5,11 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
5
5
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
6
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
7
7
|
import { clientRegistry } from "./client-registry.js";
|
|
8
|
+
import { withRequestContext } from "./request-context.js";
|
|
8
9
|
import { SmartBearMcpServer } from "./server.js";
|
|
9
10
|
import { isDraining, registerShutdownHandler } from "./shutdown.js";
|
|
10
11
|
import { getEnvVarName } from "./transport-stdio.js";
|
|
11
|
-
import {
|
|
12
|
-
class AuthorizationError extends Error {
|
|
13
|
-
}
|
|
12
|
+
import { isOptionalType, getTypeDescription } from "./zod-utils.js";
|
|
14
13
|
const PROBE_HEADERS = {
|
|
15
14
|
"Content-Type": "application/json",
|
|
16
15
|
"Cache-Control": "no-store"
|
|
@@ -281,7 +280,10 @@ async function handleStreamableHttpRequest(req, res, transports) {
|
|
|
281
280
|
);
|
|
282
281
|
return;
|
|
283
282
|
}
|
|
284
|
-
await
|
|
283
|
+
await withRequestContext(
|
|
284
|
+
req,
|
|
285
|
+
async () => await transport.handleRequest(req, res, parsedBody)
|
|
286
|
+
);
|
|
285
287
|
} catch (error) {
|
|
286
288
|
console.error("Error handling StreamableHTTP request:", error);
|
|
287
289
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
@@ -326,10 +328,13 @@ async function handleLegacyMessageRequest(req, res, url, transports) {
|
|
|
326
328
|
req.on("end", async () => {
|
|
327
329
|
try {
|
|
328
330
|
const parsedBody = JSON.parse(body);
|
|
329
|
-
await
|
|
331
|
+
await withRequestContext(
|
|
330
332
|
req,
|
|
331
|
-
|
|
332
|
-
|
|
333
|
+
async () => await session.transport.handlePostMessage(
|
|
334
|
+
req,
|
|
335
|
+
res,
|
|
336
|
+
parsedBody
|
|
337
|
+
)
|
|
333
338
|
);
|
|
334
339
|
} catch (error) {
|
|
335
340
|
console.error("Error handling POST message:", error);
|
|
@@ -338,118 +343,97 @@ async function handleLegacyMessageRequest(req, res, url, transports) {
|
|
|
338
343
|
}
|
|
339
344
|
});
|
|
340
345
|
}
|
|
341
|
-
function
|
|
342
|
-
const queryStringName = getQueryStringName(
|
|
346
|
+
function getConfigValue(clientPrefix, key, req) {
|
|
347
|
+
const queryStringName = getQueryStringName(clientPrefix, key);
|
|
343
348
|
const queryParams = querystring.parse(req.url?.split("?")[1] || "");
|
|
344
349
|
let value = queryParams[queryStringName] || queryParams[queryStringName.toLowerCase()];
|
|
345
350
|
if (typeof value === "string") {
|
|
346
351
|
return value;
|
|
347
352
|
}
|
|
348
|
-
const headerName = getHeaderName(
|
|
353
|
+
const headerName = getHeaderName(clientPrefix, key);
|
|
349
354
|
value = req.headers[headerName] || req.headers[headerName.toLowerCase()];
|
|
350
355
|
if (typeof value === "string") {
|
|
351
|
-
if (value.toLowerCase().startsWith("bearer ")) {
|
|
352
|
-
value = value.slice("bearer ".length);
|
|
353
|
-
} else if (value.toLowerCase().startsWith("token ")) {
|
|
354
|
-
value = value.slice("token ".length);
|
|
355
|
-
} else if (value.toLowerCase().startsWith("basic ")) {
|
|
356
|
-
value = value.slice("basic ".length);
|
|
357
|
-
}
|
|
358
356
|
return value;
|
|
359
357
|
}
|
|
360
|
-
const envVarName = getEnvVarName(
|
|
361
|
-
return process.env[envVarName];
|
|
362
|
-
}
|
|
363
|
-
function makeConfigFn(req) {
|
|
364
|
-
return (key, client) => resolveFromRequest(req, key, client?.configPrefix);
|
|
358
|
+
const envVarName = getEnvVarName(clientPrefix, key);
|
|
359
|
+
return process.env[envVarName] || null;
|
|
365
360
|
}
|
|
366
361
|
async function newServer(req, res) {
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
const server = new SmartBearMcpServer(configFn, enabledToolsets);
|
|
362
|
+
const enabledToolsets = getConfigValue("smartbear", "toolsets", req) || void 0;
|
|
363
|
+
const server = new SmartBearMcpServer(enabledToolsets);
|
|
370
364
|
try {
|
|
371
|
-
const configuredCount = await
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
365
|
+
const configuredCount = await withRequestContext(
|
|
366
|
+
req,
|
|
367
|
+
() => clientRegistry.configure(
|
|
368
|
+
server,
|
|
369
|
+
(client, key) => {
|
|
370
|
+
return getConfigValue(client.configPrefix, key, req);
|
|
371
|
+
},
|
|
372
|
+
true
|
|
373
|
+
// ignoreMissingRequiredConfigs
|
|
374
|
+
)
|
|
375
|
+
);
|
|
376
|
+
console.log(
|
|
377
|
+
`Configured ${configuredCount} clients for new server instance`
|
|
376
378
|
);
|
|
377
379
|
if (configuredCount === 0) {
|
|
378
380
|
throw new Error(
|
|
379
|
-
"No clients successfully configured.
|
|
381
|
+
"No clients successfully configured. Missing authentication headers."
|
|
380
382
|
);
|
|
381
383
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
+
const hasAuth = withRequestContext(
|
|
385
|
+
req,
|
|
386
|
+
() => server.getClients().some((client) => {
|
|
387
|
+
if (!client.getAuthToken) return true;
|
|
388
|
+
return client.getAuthToken() !== null;
|
|
389
|
+
})
|
|
384
390
|
);
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
throw new AuthorizationError(
|
|
391
|
+
if (!hasAuth) {
|
|
392
|
+
throw new Error(
|
|
388
393
|
"No clients have valid authentication credentials. Please authenticate via OAuth or provide alternative auth headers (e.g. API key or personal auth token)."
|
|
389
394
|
);
|
|
390
395
|
}
|
|
391
|
-
return server;
|
|
392
396
|
} catch (error) {
|
|
397
|
+
const headerHelp = getHttpHeadersHelp();
|
|
398
|
+
const errorMessage = headerHelp.length > 0 ? `Configuration error: ${error instanceof Error ? error.message : String(error)}. Please provide valid headers:
|
|
399
|
+
${headerHelp.join("\n")}` : "No clients support HTTP header configuration.";
|
|
393
400
|
const headers = {
|
|
394
401
|
"Content-Type": "text/plain"
|
|
395
402
|
};
|
|
396
|
-
if (
|
|
397
|
-
|
|
398
|
-
headers["WWW-Authenticate"] = `OAuth resource_metadata="http://${req.headers.host}/.well-known/oauth-protected-resource"`;
|
|
399
|
-
}
|
|
400
|
-
res.writeHead(401, headers);
|
|
401
|
-
res.end(error.message);
|
|
402
|
-
} else {
|
|
403
|
-
const headerHelp = getHttpHeadersHelp();
|
|
404
|
-
let errorMessage = `Configuration error: ${error instanceof Error ? error.message : String(error)}.`;
|
|
405
|
-
if (headerHelp.length > 0) {
|
|
406
|
-
errorMessage += ` Please provide valid headers:
|
|
407
|
-
${headerHelp.join("\n")}`;
|
|
408
|
-
}
|
|
409
|
-
res.writeHead(500, headers);
|
|
410
|
-
res.end(errorMessage);
|
|
403
|
+
if (req.headers.host) {
|
|
404
|
+
headers["WWW-Authenticate"] = `OAuth resource_metadata="http://${req.headers.host}/.well-known/oauth-protected-resource"`;
|
|
411
405
|
}
|
|
406
|
+
res.writeHead(401, headers);
|
|
407
|
+
res.end(errorMessage);
|
|
412
408
|
return null;
|
|
413
409
|
}
|
|
410
|
+
return server;
|
|
414
411
|
}
|
|
415
|
-
function getHeaderName(
|
|
416
|
-
|
|
417
|
-
return prefix.split(/[\s\-_]/).map(
|
|
412
|
+
function getHeaderName(clientPrefix, key) {
|
|
413
|
+
return `${clientPrefix}-${key.split("_").map(
|
|
418
414
|
(part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
|
|
419
|
-
).join("-")
|
|
415
|
+
).join("-")}`;
|
|
420
416
|
}
|
|
421
|
-
function getQueryStringName(
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
).join("");
|
|
417
|
+
function getQueryStringName(clientPrefix, key) {
|
|
418
|
+
return `${clientPrefix.toLowerCase()}${key.split("_").map(
|
|
419
|
+
(part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
|
|
420
|
+
).join("")}`;
|
|
426
421
|
}
|
|
427
422
|
function getHttpHeaders() {
|
|
428
423
|
const headers = /* @__PURE__ */ new Set();
|
|
429
|
-
for (const
|
|
430
|
-
for (const
|
|
431
|
-
|
|
432
|
-
...Object.keys(client.authenticationFields.shape)
|
|
433
|
-
]) {
|
|
434
|
-
headers.add(getHeaderName(key, client.configPrefix));
|
|
424
|
+
for (const entry of clientRegistry.getAll()) {
|
|
425
|
+
for (const configKey of Object.keys(entry.config.shape)) {
|
|
426
|
+
headers.add(getHeaderName(entry.configPrefix, configKey));
|
|
435
427
|
}
|
|
436
428
|
}
|
|
437
429
|
return Array.from(headers).sort((a, b) => a.localeCompare(b));
|
|
438
430
|
}
|
|
439
431
|
function getHttpHeadersHelp() {
|
|
440
432
|
const messages = [];
|
|
441
|
-
for (const
|
|
442
|
-
messages.push(` - ${
|
|
443
|
-
for (const [
|
|
444
|
-
|
|
445
|
-
)) {
|
|
446
|
-
const headerName = getHeaderName(authKey, client.configPrefix);
|
|
447
|
-
messages.push(` - ${headerName}: ${getTypeDescription(requirement)}`);
|
|
448
|
-
}
|
|
449
|
-
for (const [configKey, requirement] of Object.entries(
|
|
450
|
-
client.config.shape
|
|
451
|
-
)) {
|
|
452
|
-
const headerName = getHeaderName(configKey, client.configPrefix);
|
|
433
|
+
for (const entry of clientRegistry.getAll()) {
|
|
434
|
+
messages.push(` - ${entry.name}:`);
|
|
435
|
+
for (const [configKey, requirement] of Object.entries(entry.config.shape)) {
|
|
436
|
+
const headerName = getHeaderName(entry.configPrefix, configKey);
|
|
453
437
|
const requiredTag = isOptionalType(requirement) ? " (optional)" : " (required)";
|
|
454
438
|
messages.push(
|
|
455
439
|
` - ${headerName}${requiredTag}: ${getTypeDescription(requirement)}`
|
|
@@ -459,7 +443,6 @@ function getHttpHeadersHelp() {
|
|
|
459
443
|
return messages;
|
|
460
444
|
}
|
|
461
445
|
export {
|
|
462
|
-
AuthorizationError,
|
|
463
446
|
drainHttpTransport,
|
|
464
447
|
getBaseUrl,
|
|
465
448
|
getHeaderName,
|
|
@@ -4,17 +4,16 @@ import { clientRegistry } from "./client-registry.js";
|
|
|
4
4
|
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "./info.js";
|
|
5
5
|
import { SmartBearMcpServer } from "./server.js";
|
|
6
6
|
import { registerShutdownHandler } from "./shutdown.js";
|
|
7
|
-
import { getTypeDescription } from "./zod-utils.js";
|
|
8
|
-
function
|
|
7
|
+
import { isOptionalType, getTypeDescription } from "./zod-utils.js";
|
|
8
|
+
function getNoConfigMessage() {
|
|
9
9
|
const messages = [];
|
|
10
|
-
for (const
|
|
11
|
-
messages.push(` - ${
|
|
12
|
-
for (const [configKey, requirement] of
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
]) {
|
|
10
|
+
for (const entry of clientRegistry.getAll()) {
|
|
11
|
+
messages.push(` - ${entry.name}:`);
|
|
12
|
+
for (const [configKey, requirement] of Object.entries(entry.config.shape)) {
|
|
13
|
+
const envVarName = getEnvVarName(entry.configPrefix, configKey);
|
|
14
|
+
const requiredTag = isOptionalType(requirement) ? " (optional)" : " (required)";
|
|
16
15
|
messages.push(
|
|
17
|
-
` - ${
|
|
16
|
+
` - ${envVarName}${requiredTag}: ${getTypeDescription(requirement)}`
|
|
18
17
|
);
|
|
19
18
|
}
|
|
20
19
|
}
|
|
@@ -28,28 +27,27 @@ async function runStdioMode() {
|
|
|
28
27
|
console.log(
|
|
29
28
|
"The following environment variables can be set to configure each of the SmartBear clients:"
|
|
30
29
|
);
|
|
31
|
-
console.log(
|
|
30
|
+
console.log(getNoConfigMessage().join("\n"));
|
|
32
31
|
process.exit(0);
|
|
33
32
|
}
|
|
34
33
|
enableCompileCache();
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
return process.env[envVarName];
|
|
38
|
-
};
|
|
39
|
-
const server = new SmartBearMcpServer(configFn, process.env.MCP_TOOLSETS);
|
|
40
|
-
const addedCount = await clientRegistry.registerAll(
|
|
34
|
+
const server = new SmartBearMcpServer(process.env.MCP_TOOLSETS);
|
|
35
|
+
const configuredCount = await clientRegistry.configure(
|
|
41
36
|
server,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
(client, key) => {
|
|
38
|
+
const envVarName = getEnvVarName(client.configPrefix, key);
|
|
39
|
+
return process.env[envVarName] || null;
|
|
40
|
+
}
|
|
45
41
|
);
|
|
46
|
-
if (
|
|
47
|
-
const message =
|
|
42
|
+
if (configuredCount === 0) {
|
|
43
|
+
const message = getNoConfigMessage();
|
|
48
44
|
console.warn(
|
|
49
|
-
`No clients configured. Please provide valid environment variables for at least one client:
|
|
50
|
-
${message.join("\n")}`
|
|
45
|
+
message.length > 0 ? `No clients configured. Please provide valid environment variables for at least one client:
|
|
46
|
+
${message.join("\n")}` : "No clients support environment variable configuration."
|
|
51
47
|
);
|
|
52
|
-
|
|
48
|
+
for (const entry of clientRegistry.getAll()) {
|
|
49
|
+
await server.addClient(entry);
|
|
50
|
+
}
|
|
53
51
|
}
|
|
54
52
|
const transport = new StdioServerTransport();
|
|
55
53
|
registerShutdownHandler("stdio-transport", async () => {
|
|
@@ -74,9 +72,8 @@ ${message.join("\n")}`
|
|
|
74
72
|
};
|
|
75
73
|
await server.connect(transport);
|
|
76
74
|
}
|
|
77
|
-
function getEnvVarName(
|
|
78
|
-
|
|
79
|
-
return prefix.replace(/[\s\-_]/g, "_");
|
|
75
|
+
function getEnvVarName(clientPrefix, key) {
|
|
76
|
+
return `${clientPrefix.toUpperCase().replace(/-/g, "_")}_${key.toUpperCase()}`;
|
|
80
77
|
}
|
|
81
78
|
export {
|
|
82
79
|
getEnvVarName,
|
package/dist/package.json.js
CHANGED
package/dist/pactflow/client.js
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
import zod__default from "zod";
|
|
2
2
|
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
|
|
3
3
|
import { isSamplingPolyfillResult } from "../common/pollyfills.js";
|
|
4
|
+
import { getRequestHeader } from "../common/request-context.js";
|
|
4
5
|
import { ToolError } from "../common/tools.js";
|
|
5
6
|
import { getOADMatcherRecommendations, getUserMatcherSelection } from "./client/prompt-utils.js";
|
|
6
7
|
import { PROMPTS } from "./client/prompts.js";
|
|
7
8
|
import { TOOLS } from "./client/tools.js";
|
|
8
9
|
const ConfigurationSchema = zod__default.object({
|
|
9
|
-
base_url: zod__default.url().describe("Pact Broker or PactFlow base URL")
|
|
10
|
-
|
|
11
|
-
const AuthenticationSchema = zod__default.object({
|
|
12
|
-
username: zod__default.string().describe("Username for Pact Broker").optional(),
|
|
13
|
-
password: zod__default.string().describe("Password for Pact Broker").optional(),
|
|
14
|
-
token: zod__default.string().describe(
|
|
10
|
+
base_url: zod__default.url().describe("Pact Broker or PactFlow base URL"),
|
|
11
|
+
token: zod__default.string().optional().describe(
|
|
15
12
|
"Bearer token for PactFlow authentication (use this OR username/password)"
|
|
16
|
-
)
|
|
13
|
+
),
|
|
14
|
+
username: zod__default.string().optional().describe("Username for Pact Broker"),
|
|
15
|
+
password: zod__default.string().optional().describe("Password for Pact Broker")
|
|
17
16
|
});
|
|
18
17
|
class PactflowClient {
|
|
19
18
|
name = "Contract Testing";
|
|
20
19
|
capabilityPrefix = "contract-testing";
|
|
21
20
|
configPrefix = "Pact-Broker";
|
|
22
21
|
config = ConfigurationSchema;
|
|
23
|
-
|
|
22
|
+
token;
|
|
23
|
+
username;
|
|
24
|
+
password;
|
|
24
25
|
aiBaseUrl;
|
|
25
26
|
baseUrl;
|
|
26
27
|
_clientType;
|
|
@@ -31,7 +32,7 @@ class PactflowClient {
|
|
|
31
32
|
return this._server;
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
34
|
-
*
|
|
35
|
+
* Initialises the client with auth credentials and the MCP server reference.
|
|
35
36
|
* Accepts either a Bearer token (PactFlow) or username/password (Pact Broker).
|
|
36
37
|
* Does nothing if neither is supplied.
|
|
37
38
|
*
|
|
@@ -39,21 +40,23 @@ class PactflowClient {
|
|
|
39
40
|
* @param config - Connection config (base_url + token OR username/password).
|
|
40
41
|
*/
|
|
41
42
|
async configure(server, config) {
|
|
42
|
-
this.
|
|
43
|
-
|
|
43
|
+
this.token = config.token;
|
|
44
|
+
this.username = config.username;
|
|
45
|
+
this.password = config.password;
|
|
46
|
+
if (typeof config.token === "string") {
|
|
47
|
+
this._clientType = "pactflow";
|
|
48
|
+
} else if (typeof config.username === "string" && typeof config.password === "string") {
|
|
44
49
|
this._clientType = "pact_broker";
|
|
45
50
|
} else {
|
|
46
51
|
this._clientType = "pactflow";
|
|
47
52
|
}
|
|
48
53
|
this.baseUrl = config.base_url;
|
|
49
54
|
this.aiBaseUrl = `${this.baseUrl}/api/ai`;
|
|
55
|
+
this._server = server;
|
|
50
56
|
}
|
|
51
57
|
/** Returns true if the client has been configured with a base URL and credentials. */
|
|
52
58
|
isConfigured() {
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
hasAuth() {
|
|
56
|
-
return this.isConfigured() && !!this.requestHeaders;
|
|
59
|
+
return this.baseUrl !== void 0;
|
|
57
60
|
}
|
|
58
61
|
// PactFlow AI client methods
|
|
59
62
|
/**
|
|
@@ -163,28 +166,38 @@ class PactflowClient {
|
|
|
163
166
|
}
|
|
164
167
|
/** Returns the current auth/content-type headers used for all requests. */
|
|
165
168
|
get requestHeaders() {
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
let contextToken = getRequestHeader("Pact-Token") || getRequestHeader("Authorization");
|
|
170
|
+
if (Array.isArray(contextToken)) {
|
|
171
|
+
contextToken = contextToken[0];
|
|
172
|
+
}
|
|
173
|
+
if (contextToken) {
|
|
174
|
+
let authHeader = contextToken;
|
|
175
|
+
if (!contextToken.startsWith("Basic ") && !contextToken.startsWith("Bearer ")) {
|
|
176
|
+
authHeader = `Bearer ${contextToken}`;
|
|
171
177
|
}
|
|
172
178
|
return {
|
|
173
179
|
Authorization: authHeader,
|
|
174
180
|
"Content-Type": "application/json",
|
|
175
181
|
"User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
|
|
176
182
|
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
Authorization: `Basic ${Buffer.from(authString).toString("base64")}`,
|
|
184
|
-
"Content-Type": "application/json",
|
|
185
|
-
"User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
|
|
186
|
-
};
|
|
183
|
+
}
|
|
184
|
+
if (this.token) {
|
|
185
|
+
let authHeader = this.token;
|
|
186
|
+
if (!authHeader.startsWith("Basic ") && !authHeader.startsWith("Bearer ")) {
|
|
187
|
+
authHeader = `Bearer ${authHeader}`;
|
|
187
188
|
}
|
|
189
|
+
return {
|
|
190
|
+
Authorization: authHeader,
|
|
191
|
+
"Content-Type": "application/json",
|
|
192
|
+
"User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
|
|
193
|
+
};
|
|
194
|
+
} else if (this.username && this.password) {
|
|
195
|
+
const authString = `${this.username}:${this.password}`;
|
|
196
|
+
return {
|
|
197
|
+
Authorization: `Basic ${Buffer.from(authString).toString("base64")}`,
|
|
198
|
+
"Content-Type": "application/json",
|
|
199
|
+
"User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
|
|
200
|
+
};
|
|
188
201
|
}
|
|
189
202
|
return void 0;
|
|
190
203
|
}
|
package/dist/qmetry/client.js
CHANGED
|
@@ -1,45 +1,43 @@
|
|
|
1
1
|
import zod__default from "zod";
|
|
2
|
+
import { getRequestHeader } from "../common/request-context.js";
|
|
2
3
|
import { findAutoResolveConfig, autoResolveViewIdAndFolderPath } from "./client/auto-resolve.js";
|
|
3
4
|
import { QMETRY_HANDLER_MAP } from "./client/handlers.js";
|
|
4
5
|
import { getProjectInfo } from "./client/project.js";
|
|
5
6
|
import { TOOLS } from "./client/tools/index.js";
|
|
6
7
|
import { QMETRY_DEFAULTS } from "./config/constants.js";
|
|
7
8
|
const ConfigurationSchema = zod__default.object({
|
|
9
|
+
api_key: zod__default.string().describe("QMetry API key for authentication"),
|
|
8
10
|
base_url: zod__default.string().url().optional().describe(
|
|
9
11
|
"Optional QMetry base URL for custom or region-specific endpoints"
|
|
10
12
|
)
|
|
11
13
|
});
|
|
12
|
-
const AuthenticationSchema = zod__default.object({
|
|
13
|
-
api_key: zod__default.string().describe("QMetry API key for authentication").optional()
|
|
14
|
-
});
|
|
15
14
|
class QmetryClient {
|
|
16
15
|
name = "QMetry";
|
|
17
16
|
capabilityPrefix = "qmetry";
|
|
18
17
|
configPrefix = "Qmetry";
|
|
19
18
|
config = ConfigurationSchema;
|
|
20
|
-
|
|
19
|
+
token;
|
|
21
20
|
projectApiKey = QMETRY_DEFAULTS.PROJECT_KEY;
|
|
22
21
|
endpoint = QMETRY_DEFAULTS.BASE_URL;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
this.server = server;
|
|
22
|
+
async configure(_server, config, _cache) {
|
|
23
|
+
this.token = config.api_key;
|
|
26
24
|
if (config.base_url) {
|
|
27
25
|
this.endpoint = config.base_url;
|
|
28
26
|
}
|
|
29
27
|
}
|
|
30
28
|
isConfigured() {
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
hasAuth() {
|
|
34
|
-
return this.isConfigured() && !!this.getAuthToken();
|
|
35
|
-
}
|
|
36
|
-
getAuthToken() {
|
|
37
|
-
return this.server?.getEnv("api_key", this);
|
|
29
|
+
return true;
|
|
38
30
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
31
|
+
getToken() {
|
|
32
|
+
let contextToken = getRequestHeader("Qmetry-Token") || getRequestHeader("apikey");
|
|
33
|
+
if (Array.isArray(contextToken)) {
|
|
34
|
+
contextToken = contextToken[0];
|
|
35
|
+
}
|
|
36
|
+
if (contextToken) {
|
|
37
|
+
return contextToken;
|
|
38
|
+
}
|
|
39
|
+
if (!this.token) throw new Error("Client not configured");
|
|
40
|
+
return this.token;
|
|
43
41
|
}
|
|
44
42
|
getBaseUrl() {
|
|
45
43
|
return this.endpoint;
|
|
@@ -86,7 +84,7 @@ class QmetryClient {
|
|
|
86
84
|
let projectInfo;
|
|
87
85
|
try {
|
|
88
86
|
projectInfo = await getProjectInfo(
|
|
89
|
-
this.
|
|
87
|
+
this.getToken(),
|
|
90
88
|
baseUrl,
|
|
91
89
|
projectKey
|
|
92
90
|
);
|
|
@@ -107,7 +105,7 @@ class QmetryClient {
|
|
|
107
105
|
}
|
|
108
106
|
const { projectKey: _, baseUrl: __, ...cleanArgs } = a;
|
|
109
107
|
const result = await handlerFn(
|
|
110
|
-
this.
|
|
108
|
+
this.getToken(),
|
|
111
109
|
baseUrl,
|
|
112
110
|
projectKey,
|
|
113
111
|
cleanArgs
|
package/dist/qtm4j/client.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import zod__default from "zod";
|
|
2
|
+
import { getRequestHeader } from "../common/request-context.js";
|
|
2
3
|
import { CONFIG_KEYS, API_CONFIG, SCHEMA_DESCRIPTIONS, CLIENT_CONFIG, ERROR_MESSAGES } from "./config/constants.js";
|
|
3
4
|
import { ApiClient } from "./http/api-client.js";
|
|
4
5
|
import { ResolverRegistry } from "./resolver/resolver-registry.js";
|
|
5
6
|
const ConfigurationSchema = zod__default.object({
|
|
7
|
+
[CONFIG_KEYS.API_KEY]: zod__default.string().describe(SCHEMA_DESCRIPTIONS.API_KEY),
|
|
8
|
+
[CONFIG_KEYS.AUTOMATION_API_KEY]: zod__default.string().optional().describe(SCHEMA_DESCRIPTIONS.AUTOMATION_API_KEY),
|
|
6
9
|
[CONFIG_KEYS.BASE_URL]: zod__default.string().url().optional().default(API_CONFIG.DEFAULT_BASE_URL).describe(SCHEMA_DESCRIPTIONS.BASE_URL)
|
|
7
10
|
});
|
|
8
|
-
const AuthenticationSchema = zod__default.object({
|
|
9
|
-
api_key: zod__default.string().describe(SCHEMA_DESCRIPTIONS.API_KEY).optional(),
|
|
10
|
-
automation_api_key: zod__default.string().describe(SCHEMA_DESCRIPTIONS.AUTOMATION_API_KEY).optional()
|
|
11
|
-
});
|
|
12
11
|
class Qtm4jClient {
|
|
13
12
|
name = CLIENT_CONFIG.NAME;
|
|
14
13
|
capabilityPrefix = CLIENT_CONFIG.TOOL_PREFIX;
|
|
15
14
|
configPrefix = CLIENT_CONFIG.CONFIG_PREFIX;
|
|
16
15
|
config = ConfigurationSchema;
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
_apiKey;
|
|
17
|
+
_automationApiKey;
|
|
19
18
|
baseUrl = API_CONFIG.DEFAULT_BASE_URL;
|
|
20
19
|
apiClient;
|
|
21
20
|
resolverRegistry;
|
|
@@ -25,7 +24,8 @@ class Qtm4jClient {
|
|
|
25
24
|
* @param config - Configuration object containing API key and optional base URL
|
|
26
25
|
*/
|
|
27
26
|
async configure(server, config) {
|
|
28
|
-
this.
|
|
27
|
+
this._apiKey = config[CONFIG_KEYS.API_KEY];
|
|
28
|
+
this._automationApiKey = config[CONFIG_KEYS.AUTOMATION_API_KEY];
|
|
29
29
|
if (config[CONFIG_KEYS.BASE_URL]) {
|
|
30
30
|
this.baseUrl = config[CONFIG_KEYS.BASE_URL];
|
|
31
31
|
}
|
|
@@ -40,20 +40,34 @@ class Qtm4jClient {
|
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
43
|
+
* Get authentication token with request-scoped override support
|
|
44
|
+
* Checks request headers first, then falls back to configured API key
|
|
45
|
+
* @returns API key or null if not found
|
|
45
46
|
*/
|
|
46
|
-
isConfigured() {
|
|
47
|
-
return !!this.apiClient;
|
|
48
|
-
}
|
|
49
|
-
hasAuth() {
|
|
50
|
-
return this.isConfigured() && !!this.getAuthToken();
|
|
51
|
-
}
|
|
52
47
|
getAuthToken() {
|
|
53
|
-
|
|
48
|
+
const contextHeader = getRequestHeader("Qtm4j-Api-Key") || getRequestHeader("apiKey") || getRequestHeader("Authorization");
|
|
49
|
+
if (contextHeader) {
|
|
50
|
+
let token = Array.isArray(contextHeader) ? contextHeader[0] : contextHeader;
|
|
51
|
+
if (token.startsWith("Bearer ")) {
|
|
52
|
+
token = token.substring(7);
|
|
53
|
+
}
|
|
54
|
+
return token;
|
|
55
|
+
}
|
|
56
|
+
return this._apiKey || null;
|
|
54
57
|
}
|
|
55
58
|
getAutomationApiKey() {
|
|
56
|
-
|
|
59
|
+
const headerKey = getRequestHeader("Qtm4j-Automation-Api-Key");
|
|
60
|
+
if (headerKey) {
|
|
61
|
+
return Array.isArray(headerKey) ? headerKey[0] : headerKey;
|
|
62
|
+
}
|
|
63
|
+
return this._automationApiKey || null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Check if the client is properly configured
|
|
67
|
+
* @returns true if API key is set and client is ready
|
|
68
|
+
*/
|
|
69
|
+
isConfigured() {
|
|
70
|
+
return this.apiClient !== void 0;
|
|
57
71
|
}
|
|
58
72
|
/**
|
|
59
73
|
* Get the configured API client instance
|
package/dist/reflect/client.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
|
|
3
|
+
import { getRequestHeader } from "../common/request-context.js";
|
|
3
4
|
import { ToolError } from "../common/tools.js";
|
|
4
|
-
import { API_KEY_HEADER, AUTHORIZATION_HEADER } from "./config/constants.js";
|
|
5
|
+
import { API_KEY_HEADER, REFLECT_API_TOKEN_HEADER, AUTHORIZATION_HEADER } from "./config/constants.js";
|
|
5
6
|
import { SapTest } from "./prompt/sap-test.js";
|
|
6
7
|
import { AddPromptStep } from "./tool/recording/add-prompt-step.js";
|
|
7
8
|
import { AddSegment } from "./tool/recording/add-segment.js";
|
|
@@ -17,12 +18,11 @@ import { GetTestStatus } from "./tool/tests/get-test-status.js";
|
|
|
17
18
|
import { ListSegments } from "./tool/tests/list-segments.js";
|
|
18
19
|
import { ListTests } from "./tool/tests/list-tests.js";
|
|
19
20
|
import { RunTest } from "./tool/tests/run-test.js";
|
|
20
|
-
const ConfigurationSchema = z.object({
|
|
21
|
-
|
|
22
|
-
api_token: z.string().describe("Reflect API authentication token").optional()
|
|
21
|
+
const ConfigurationSchema = z.object({
|
|
22
|
+
api_token: z.string().describe("Reflect API authentication token")
|
|
23
23
|
});
|
|
24
24
|
class ReflectClient {
|
|
25
|
-
|
|
25
|
+
_apiToken;
|
|
26
26
|
activeConnections = /* @__PURE__ */ new Map();
|
|
27
27
|
sessionStates = /* @__PURE__ */ new Map();
|
|
28
28
|
mcpSessionConnections = /* @__PURE__ */ new Map();
|
|
@@ -30,24 +30,33 @@ class ReflectClient {
|
|
|
30
30
|
capabilityPrefix = "reflect";
|
|
31
31
|
configPrefix = "Reflect";
|
|
32
32
|
config = ConfigurationSchema;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
this._server = server;
|
|
33
|
+
async configure(_server, config, _cache) {
|
|
34
|
+
this._apiToken = config.api_token;
|
|
36
35
|
}
|
|
37
36
|
getAuthToken() {
|
|
38
|
-
|
|
37
|
+
const contextHeader = getRequestHeader(REFLECT_API_TOKEN_HEADER) || getRequestHeader(API_KEY_HEADER) || getRequestHeader(AUTHORIZATION_HEADER);
|
|
38
|
+
if (contextHeader) {
|
|
39
|
+
let token = Array.isArray(contextHeader) ? contextHeader[0] : contextHeader;
|
|
40
|
+
if (token.startsWith("Bearer ")) {
|
|
41
|
+
token = token.substring(7);
|
|
42
|
+
}
|
|
43
|
+
return token;
|
|
44
|
+
}
|
|
45
|
+
return this._apiToken || null;
|
|
39
46
|
}
|
|
40
47
|
isConfigured() {
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
hasAuth() {
|
|
44
|
-
return this.isConfigured() && !!this.getAuthToken();
|
|
48
|
+
return true;
|
|
45
49
|
}
|
|
46
50
|
isOAuthRequest() {
|
|
47
|
-
if (
|
|
51
|
+
if (getRequestHeader(REFLECT_API_TOKEN_HEADER) || getRequestHeader(API_KEY_HEADER)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const authHeader = getRequestHeader(AUTHORIZATION_HEADER);
|
|
55
|
+
if (!authHeader) {
|
|
48
56
|
return false;
|
|
49
57
|
}
|
|
50
|
-
|
|
58
|
+
const headerValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
59
|
+
return headerValue.toLowerCase().startsWith("bearer ");
|
|
51
60
|
}
|
|
52
61
|
getAuthHeader() {
|
|
53
62
|
const token = this.getAuthToken();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const API_KEY_HEADER = "X-API-KEY";
|
|
2
|
+
const REFLECT_API_TOKEN_HEADER = "Reflect-Api-Token";
|
|
2
3
|
const AUTHORIZATION_HEADER = "Authorization";
|
|
3
4
|
const API_HOSTNAME = "api.reflect.run";
|
|
4
5
|
const WEBSOCKET_HOSTNAME = "recording.us-east-1.reflect.run";
|
|
@@ -7,6 +8,7 @@ export {
|
|
|
7
8
|
API_HOSTNAME,
|
|
8
9
|
API_KEY_HEADER,
|
|
9
10
|
AUTHORIZATION_HEADER,
|
|
11
|
+
REFLECT_API_TOKEN_HEADER,
|
|
10
12
|
WEBSOCKET_HOSTNAME,
|
|
11
13
|
WEB_APP_HOSTNAME
|
|
12
14
|
};
|
package/dist/swagger/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
|
|
3
|
+
import { getRequestHeader } from "../common/request-context.js";
|
|
3
4
|
import "./config-utils.js";
|
|
4
5
|
import { SwaggerAPI } from "./client/api.js";
|
|
5
6
|
import { SwaggerConfiguration } from "./client/configuration.js";
|
|
@@ -8,45 +9,47 @@ import "./client/registry-types.js";
|
|
|
8
9
|
import { TOOLS } from "./client/tools.js";
|
|
9
10
|
import "./client/user-management-types.js";
|
|
10
11
|
const ConfigurationSchema = z.object({
|
|
12
|
+
api_key: z.string().describe("Swagger API key for authentication"),
|
|
11
13
|
portal_base_path: z.string().optional().describe("Base path for Portal API requests (optional)"),
|
|
12
14
|
registry_base_path: z.string().optional().describe("Base path for Registry API requests (optional)"),
|
|
13
15
|
ui_base_path: z.string().optional().describe("Base URL for the SwaggerHub UI (optional)")
|
|
14
16
|
});
|
|
15
|
-
const AuthenticationSchema = z.object({
|
|
16
|
-
api_key: z.string().describe("Swagger API key for authentication").optional()
|
|
17
|
-
});
|
|
18
17
|
class SwaggerClient {
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
api;
|
|
19
|
+
_apiKey;
|
|
21
20
|
name = "Swagger";
|
|
22
21
|
capabilityPrefix = "swagger";
|
|
23
22
|
configPrefix = "Swagger";
|
|
24
23
|
config = ConfigurationSchema;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
this.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return this.apiConfig !== void 0;
|
|
24
|
+
async configure(_server, config, _cache) {
|
|
25
|
+
this._apiKey = config.api_key;
|
|
26
|
+
this.api = new SwaggerAPI(
|
|
27
|
+
new SwaggerConfiguration({
|
|
28
|
+
token: () => this.getAuthToken(),
|
|
29
|
+
portalBasePath: config.portal_base_path,
|
|
30
|
+
registryBasePath: config.registry_base_path,
|
|
31
|
+
uiBasePath: config.ui_base_path
|
|
32
|
+
}),
|
|
33
|
+
`${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
|
|
34
|
+
);
|
|
37
35
|
}
|
|
38
36
|
getAuthToken() {
|
|
39
|
-
|
|
37
|
+
const contextHeader = getRequestHeader("Swagger-Api-Key") || getRequestHeader("Authorization");
|
|
38
|
+
if (contextHeader) {
|
|
39
|
+
let token = Array.isArray(contextHeader) ? contextHeader[0] : contextHeader;
|
|
40
|
+
if (token.startsWith("Bearer ")) {
|
|
41
|
+
token = token.substring(7);
|
|
42
|
+
}
|
|
43
|
+
return token;
|
|
44
|
+
}
|
|
45
|
+
return this._apiKey || null;
|
|
40
46
|
}
|
|
41
|
-
|
|
42
|
-
return this.
|
|
47
|
+
isConfigured() {
|
|
48
|
+
return this.api !== void 0;
|
|
43
49
|
}
|
|
44
50
|
getApi() {
|
|
45
|
-
if (!this.
|
|
46
|
-
return
|
|
47
|
-
this.apiConfig,
|
|
48
|
-
`${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`
|
|
49
|
-
);
|
|
51
|
+
if (!this.api) throw new Error("Client not configured");
|
|
52
|
+
return this.api;
|
|
50
53
|
}
|
|
51
54
|
// Delegate API methods to the SwaggerAPI instance
|
|
52
55
|
async getPortals() {
|
package/dist/zephyr/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import zod__default from "zod";
|
|
2
|
+
import { getRequestHeader } from "../common/request-context.js";
|
|
2
3
|
import { ApiClient } from "./common/api-client.js";
|
|
3
4
|
import { GetEnvironments } from "./tool/environment/get-environments.js";
|
|
4
5
|
import { CreateFolder } from "./tool/folder/create-folder.js";
|
|
@@ -35,32 +36,38 @@ import { GetTestExecutions } from "./tool/test-execution/get-test-executions.js"
|
|
|
35
36
|
import { GetTestExecutionSteps } from "./tool/test-execution/get-test-steps.js";
|
|
36
37
|
import { UpdateTestExecution } from "./tool/test-execution/update-test-execution.js";
|
|
37
38
|
import { UpdateTestExecutionSteps } from "./tool/test-execution/update-test-steps.js";
|
|
39
|
+
const BASE_URL_DEFAULT = "https://api.zephyrscale.smartbear.com/v2";
|
|
38
40
|
const ConfigurationSchema = zod__default.object({
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const AuthenticationSchema = zod__default.object({
|
|
42
|
-
api_token: zod__default.string().describe("Zephyr Scale API token for authentication").optional()
|
|
41
|
+
api_token: zod__default.string().describe("Zephyr Scale API token for authentication"),
|
|
42
|
+
base_url: zod__default.string().url().optional().describe("Zephyr Scale API base URL").default(BASE_URL_DEFAULT)
|
|
43
43
|
});
|
|
44
44
|
class ZephyrClient {
|
|
45
45
|
apiClient;
|
|
46
|
-
|
|
46
|
+
_apiToken;
|
|
47
47
|
name = "Zephyr";
|
|
48
48
|
capabilityPrefix = "zephyr";
|
|
49
49
|
configPrefix = "Zephyr";
|
|
50
50
|
config = ConfigurationSchema;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
this.
|
|
54
|
-
|
|
51
|
+
async configure(_server, config, _cache) {
|
|
52
|
+
this._apiToken = config.api_token;
|
|
53
|
+
this.apiClient = new ApiClient(
|
|
54
|
+
() => this.getAuthToken(),
|
|
55
|
+
config.base_url || process.env.ZEPHYR_CUSTOM_BASE_URL || BASE_URL_DEFAULT
|
|
56
|
+
);
|
|
55
57
|
}
|
|
56
58
|
getAuthToken() {
|
|
57
|
-
|
|
59
|
+
const contextHeader = getRequestHeader("Zephyr-Api-Token") || getRequestHeader("Authorization");
|
|
60
|
+
if (contextHeader) {
|
|
61
|
+
let token = Array.isArray(contextHeader) ? contextHeader[0] : contextHeader;
|
|
62
|
+
if (token.startsWith("Bearer ")) {
|
|
63
|
+
token = token.substring(7);
|
|
64
|
+
}
|
|
65
|
+
return token;
|
|
66
|
+
}
|
|
67
|
+
return this._apiToken || null;
|
|
58
68
|
}
|
|
59
69
|
isConfigured() {
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
hasAuth() {
|
|
63
|
-
return this.isConfigured() && !!this.getAuthToken();
|
|
70
|
+
return this.apiClient !== void 0;
|
|
64
71
|
}
|
|
65
72
|
getApiClient() {
|
|
66
73
|
if (!this.apiClient) throw new Error("Client not configured");
|