@smartbear/mcp 0.25.0 → 0.26.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 +12 -3
- package/dist/bearq/client.js +15 -14
- package/dist/bugsnag/client.js +46 -25
- package/dist/collaborator/client.js +21 -15
- package/dist/common/client-registry.js +20 -46
- package/dist/common/info.js +5 -1
- 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 +26 -29
- package/dist/package.json.js +1 -1
- package/dist/pactflow/client.js +45 -32
- package/dist/qmetry/client/api/client-api.js +2 -2
- package/dist/qmetry/client/automation.js +2 -2
- package/dist/qmetry/client.js +18 -20
- package/dist/qtm4j/client.js +31 -17
- package/dist/qtm4j/http/auth-service.js +2 -2
- package/dist/reflect/client.js +28 -17
- package/dist/reflect/config/constants.js +2 -0
- package/dist/reflect/tool/tests/get-test-detail.js +37 -0
- package/dist/swagger/client/functional-testing-api.js +36 -0
- package/dist/swagger/client/functional-testing-tools.js +11 -0
- package/dist/swagger/client/tools.js +3 -1
- package/dist/swagger/client.js +66 -27
- package/dist/zephyr/client.js +21 -14
- package/dist/zephyr/common/auth-service.js +2 -2
- package/package.json +2 -1
|
@@ -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,
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import { enableCompileCache } from "node:module";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { clientRegistry } from "./client-registry.js";
|
|
4
|
-
import {
|
|
4
|
+
import { USER_AGENT } 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
|
}
|
|
@@ -22,34 +21,33 @@ function getConfigMessage() {
|
|
|
22
21
|
}
|
|
23
22
|
async function runStdioMode() {
|
|
24
23
|
if (process.argv.includes("--version")) {
|
|
25
|
-
console.log(
|
|
24
|
+
console.log(`User-Agent: ${USER_AGENT}`);
|
|
26
25
|
process.exit(0);
|
|
27
26
|
} else if (process.argv.includes("--help")) {
|
|
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
|
-
import {
|
|
2
|
+
import { USER_AGENT } 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
|
-
"User-Agent":
|
|
181
|
+
"User-Agent": USER_AGENT
|
|
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": USER_AGENT
|
|
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": USER_AGENT
|
|
200
|
+
};
|
|
188
201
|
}
|
|
189
202
|
return void 0;
|
|
190
203
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { USER_AGENT } from "../../../common/info.js";
|
|
2
2
|
import { QMETRY_DEFAULTS } from "../../config/constants.js";
|
|
3
3
|
import { handleQMetryFetchError, handleQMetryApiError } from "./error-handler.js";
|
|
4
4
|
async function qmetryRequest({
|
|
@@ -13,7 +13,7 @@ async function qmetryRequest({
|
|
|
13
13
|
const headers = {
|
|
14
14
|
apikey: token,
|
|
15
15
|
project: project || QMETRY_DEFAULTS.PROJECT_KEY,
|
|
16
|
-
"User-Agent":
|
|
16
|
+
"User-Agent": USER_AGENT,
|
|
17
17
|
"qmetry-source": "smartbear-mcp"
|
|
18
18
|
};
|
|
19
19
|
if (body) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { USER_AGENT } from "../../common/info.js";
|
|
2
2
|
import { QMETRY_DEFAULTS } from "../config/constants.js";
|
|
3
3
|
import { QMETRY_PATHS } from "../config/rest-endpoints.js";
|
|
4
4
|
import { DEFAULT_IMPORT_AUTOMATION_PAYLOAD } from "../types/automation.js";
|
|
@@ -91,7 +91,7 @@ async function importAutomationResults(token, baseUrl, project, payload) {
|
|
|
91
91
|
const headers = {
|
|
92
92
|
apikey: token,
|
|
93
93
|
project: finalPayload.projectID || project || QMETRY_DEFAULTS.PROJECT_KEY,
|
|
94
|
-
"User-Agent":
|
|
94
|
+
"User-Agent": USER_AGENT,
|
|
95
95
|
"qmetry-source": "smartbear-mcp"
|
|
96
96
|
// Note: Content-Type will be set automatically by fetch for FormData
|
|
97
97
|
};
|
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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { USER_AGENT } from "../../common/info.js";
|
|
2
2
|
import { CONTENT_TYPES, HTTP_HEADERS } from "../config/constants.js";
|
|
3
3
|
class AuthService {
|
|
4
4
|
apiKey;
|
|
@@ -13,7 +13,7 @@ class AuthService {
|
|
|
13
13
|
return {
|
|
14
14
|
[HTTP_HEADERS.API_KEY]: this.apiKey,
|
|
15
15
|
[HTTP_HEADERS.CONTENT_TYPE]: CONTENT_TYPES.JSON,
|
|
16
|
-
[HTTP_HEADERS.USER_AGENT]:
|
|
16
|
+
[HTTP_HEADERS.USER_AGENT]: USER_AGENT,
|
|
17
17
|
[HTTP_HEADERS.ACCEPT]: CONTENT_TYPES.JSON
|
|
18
18
|
};
|
|
19
19
|
}
|