@smartbear/mcp 0.23.0 → 0.25.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 +10 -2
- package/dist/bearq/client.js +12 -13
- package/dist/bearq/tool/tasks/chat-with-qa-lead.js +1 -0
- package/dist/bearq/tool/tasks/expand-application-model.js +1 -0
- package/dist/bearq/tool/tasks/get-task-status.js +1 -0
- package/dist/bearq/tool/tasks/get-task.js +1 -0
- package/dist/bearq/tool/tasks/refine-all-draft-tests.js +1 -0
- package/dist/bearq/tool/tasks/refine-test-cases.js +1 -0
- package/dist/bearq/tool/tasks/refine-tests-in-functional-areas.js +1 -0
- package/dist/bearq/tool/tasks/run-regression-tests.js +1 -0
- package/dist/bearq/tool/tasks/run-test-cases.js +1 -0
- package/dist/bearq/tool/tasks/run-tests-in-functional-areas.js +1 -0
- package/dist/bearq/tool/tasks/stop-task.js +1 -0
- package/dist/bearq/tool/tasks/wait-for-task.js +1 -0
- package/dist/bugsnag/client.js +24 -44
- package/dist/bugsnag/tool/error/get-error.js +1 -0
- package/dist/bugsnag/tool/error/list-project-errors.js +1 -0
- package/dist/bugsnag/tool/error/update-error.js +1 -0
- package/dist/bugsnag/tool/event/get-event-details-from-dashboard-url.js +1 -0
- package/dist/bugsnag/tool/event/get-event.js +1 -0
- package/dist/bugsnag/tool/event/list-error-events.js +1 -0
- package/dist/bugsnag/tool/performance/get-network-endpoint-groupings.js +1 -0
- package/dist/bugsnag/tool/performance/get-span-group.js +1 -0
- package/dist/bugsnag/tool/performance/get-trace.js +1 -0
- package/dist/bugsnag/tool/performance/list-span-groups.js +1 -0
- package/dist/bugsnag/tool/performance/list-spans.js +1 -0
- package/dist/bugsnag/tool/performance/list-trace-fields.js +1 -0
- package/dist/bugsnag/tool/performance/set-network-endpoint-groupings.js +1 -0
- package/dist/bugsnag/tool/project/get-current-project.js +1 -0
- package/dist/bugsnag/tool/project/list-project-event-filters.js +1 -0
- package/dist/bugsnag/tool/project/list-projects.js +1 -0
- package/dist/bugsnag/tool/release/get-build.js +1 -0
- package/dist/bugsnag/tool/release/get-release.js +1 -0
- package/dist/bugsnag/tool/release/list-releases.js +1 -0
- package/dist/collaborator/client.js +24 -19
- package/dist/common/client-registry.js +63 -29
- package/dist/common/server.js +57 -1
- package/dist/common/transport-http.js +90 -69
- package/dist/common/transport-stdio.js +29 -24
- package/dist/package.json.js +1 -1
- package/dist/pactflow/client/base.js +3 -3
- package/dist/pactflow/client/tools.js +102 -0
- package/dist/pactflow/client.js +30 -43
- package/dist/qmetry/client/tools/automation-tools.js +2 -0
- package/dist/qmetry/client/tools/issue-tools.js +6 -0
- package/dist/qmetry/client/tools/project-tools.js +9 -0
- package/dist/qmetry/client/tools/requirement-tools.js +5 -0
- package/dist/qmetry/client/tools/testcase-tools.js +7 -0
- package/dist/qmetry/client/tools/testsuite-tools.js +11 -0
- package/dist/qmetry/client.js +20 -18
- package/dist/qtm4j/client.js +57 -23
- package/dist/qtm4j/config/constants.js +197 -5
- package/dist/qtm4j/config/field-resolution.types.js +5 -2
- package/dist/qtm4j/http/api-client.js +90 -3
- package/dist/qtm4j/resolver/cache/cache.js +1 -1
- package/dist/qtm4j/resolver/resolver-registry.js +7 -1
- package/dist/qtm4j/resolver/resolvers/common-attribute-resolver.js +1 -0
- package/dist/qtm4j/resolver/resolvers/component-resolver.js +2 -0
- package/dist/qtm4j/resolver/resolvers/label-resolver.js +2 -0
- package/dist/qtm4j/resolver/resolvers/requirement-id-resolver.js +28 -0
- package/dist/qtm4j/resolver/resolvers/test-cycle-uid-resolver.js +28 -0
- package/dist/qtm4j/schema/automation.schema.js +107 -0
- package/dist/qtm4j/schema/linked-items.schema.js +95 -0
- package/dist/qtm4j/schema/requirements.schema.js +109 -0
- package/dist/qtm4j/schema/search-test-cycle.schema.js +133 -0
- package/dist/qtm4j/schema/test-cycle.link.schema.js +260 -0
- package/dist/qtm4j/schema/test-cycle.schema.js +39 -0
- package/dist/qtm4j/schema/update-test-cycle.schema.js +54 -0
- package/dist/qtm4j/tool/project/get-projects.js +2 -1
- package/dist/qtm4j/tool/project/set-project-context.js +2 -1
- package/dist/qtm4j/tool/requirement/get-linked-testcases.js +93 -0
- package/dist/qtm4j/tool/requirement/link-testcases.js +107 -0
- package/dist/qtm4j/tool/requirement/unlink-testcases.js +97 -0
- package/dist/qtm4j/tool/test-automation/get-automation-history.js +70 -0
- package/dist/qtm4j/tool/test-automation/upload-automation-result.js +144 -0
- package/dist/qtm4j/tool/test-case/create-test-case.js +2 -1
- package/dist/qtm4j/tool/test-case/get-linked-requirements.js +67 -0
- package/dist/qtm4j/tool/test-case/get-test-cases.js +2 -1
- package/dist/qtm4j/tool/test-case/get-test-steps.js +2 -1
- package/dist/qtm4j/tool/test-case/link-requirements.js +124 -0
- package/dist/qtm4j/tool/test-case/unlink-requirements.js +116 -0
- package/dist/qtm4j/tool/test-case/update-test-case.js +2 -1
- package/dist/qtm4j/tool/test-cycle/create-test-cycle.js +81 -0
- package/dist/qtm4j/tool/test-cycle/get-linked-requirements.js +71 -0
- package/dist/qtm4j/tool/test-cycle/link-requirements.js +91 -0
- package/dist/qtm4j/tool/test-cycle/link-testcases.js +118 -0
- package/dist/qtm4j/tool/test-cycle/search-linked-testcases.js +114 -0
- package/dist/qtm4j/tool/test-cycle/search-test-cycle.js +137 -0
- package/dist/qtm4j/tool/test-cycle/unlink-requirements.js +87 -0
- package/dist/qtm4j/tool/test-cycle/unlink-testcases.js +103 -0
- package/dist/qtm4j/tool/test-cycle/update-test-cycle.js +162 -0
- package/dist/reflect/client.js +15 -24
- package/dist/reflect/config/constants.js +0 -2
- package/dist/reflect/tool/recording/add-prompt-step.js +1 -0
- package/dist/reflect/tool/recording/add-segment.js +1 -0
- package/dist/reflect/tool/recording/connect-to-session.js +1 -0
- package/dist/reflect/tool/recording/delete-previous-step.js +1 -0
- package/dist/reflect/tool/recording/get-screenshot.js +1 -0
- package/dist/reflect/tool/suites/cancel-suite-execution.js +1 -0
- package/dist/reflect/tool/suites/execute-suite.js +1 -0
- package/dist/reflect/tool/suites/get-suite-execution-status.js +1 -0
- package/dist/reflect/tool/suites/list-suite-executions.js +1 -0
- package/dist/reflect/tool/suites/list-suites.js +1 -0
- package/dist/reflect/tool/tests/get-test-status.js +1 -0
- package/dist/reflect/tool/tests/list-segments.js +1 -0
- package/dist/reflect/tool/tests/list-tests.js +1 -0
- package/dist/reflect/tool/tests/run-test.js +1 -0
- package/dist/swagger/client/portal-types.js +1 -1
- package/dist/swagger/client/tools.js +23 -0
- package/dist/swagger/client.js +25 -28
- package/dist/zephyr/client.js +14 -21
- package/dist/zephyr/tool/environment/get-environments.js +1 -0
- package/dist/zephyr/tool/folder/create-folder.js +1 -0
- package/dist/zephyr/tool/issue-link/get-test-cases.js +1 -0
- package/dist/zephyr/tool/issue-link/get-test-cycles.js +1 -0
- package/dist/zephyr/tool/issue-link/get-test-executions.js +1 -0
- package/dist/zephyr/tool/priority/get-priorities.js +1 -0
- package/dist/zephyr/tool/project/get-project.js +1 -0
- package/dist/zephyr/tool/project/get-projects.js +1 -0
- package/dist/zephyr/tool/status/get-statuses.js +1 -0
- package/dist/zephyr/tool/test-case/create-issue-link.js +1 -0
- package/dist/zephyr/tool/test-case/create-test-case.js +1 -0
- package/dist/zephyr/tool/test-case/create-test-script.js +1 -0
- package/dist/zephyr/tool/test-case/create-test-steps.js +1 -0
- package/dist/zephyr/tool/test-case/create-web-link.js +1 -0
- package/dist/zephyr/tool/test-case/get-links.js +1 -0
- package/dist/zephyr/tool/test-case/get-test-case.js +1 -0
- package/dist/zephyr/tool/test-case/get-test-cases.js +1 -0
- package/dist/zephyr/tool/test-case/get-test-script.js +1 -0
- package/dist/zephyr/tool/test-case/get-test-steps.js +1 -0
- package/dist/zephyr/tool/test-case/update-test-case.js +1 -0
- package/dist/zephyr/tool/test-cycle/create-issue-link.js +1 -0
- package/dist/zephyr/tool/test-cycle/create-test-cycle.js +1 -0
- package/dist/zephyr/tool/test-cycle/create-web-link.js +1 -0
- package/dist/zephyr/tool/test-cycle/get-links.js +1 -0
- package/dist/zephyr/tool/test-cycle/get-test-cycle.js +1 -0
- package/dist/zephyr/tool/test-cycle/get-test-cycles.js +1 -0
- package/dist/zephyr/tool/test-cycle/update-test-cycle.js +1 -0
- package/dist/zephyr/tool/test-execution/create-issue-link.js +1 -0
- package/dist/zephyr/tool/test-execution/create-test-execution.js +1 -0
- package/dist/zephyr/tool/test-execution/get-test-execution-links.js +1 -0
- package/dist/zephyr/tool/test-execution/get-test-execution.js +1 -0
- package/dist/zephyr/tool/test-execution/get-test-executions.js +1 -0
- package/dist/zephyr/tool/test-execution/get-test-steps.js +1 -0
- package/dist/zephyr/tool/test-execution/update-test-execution.js +1 -0
- package/dist/zephyr/tool/test-execution/update-test-steps.js +1 -0
- package/package.json +1 -1
- package/dist/common/request-context.js +0 -20
|
@@ -1,28 +1,36 @@
|
|
|
1
|
-
import { ZodURL } from "zod";
|
|
1
|
+
import { ZodURL, ZodError } from "zod";
|
|
2
2
|
import { fullyUnwrapZodType, isOptionalType } from "./zod-utils.js";
|
|
3
3
|
class ClientRegistry {
|
|
4
4
|
entries = [];
|
|
5
|
-
enabledClients
|
|
5
|
+
enabledClients;
|
|
6
6
|
/**
|
|
7
|
-
* Configure which clients should be enabled based on MCP_CLIENTS env var
|
|
7
|
+
* Configure which clients should be enabled based on MCP_CLIENTS env var and MCP_TOOLSETS (to enable any referenced clients)
|
|
8
8
|
* If not set or empty, all clients are enabled
|
|
9
9
|
* If set, should be comma-separated list of client names (case-insensitive)
|
|
10
10
|
*/
|
|
11
11
|
constructor() {
|
|
12
|
-
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
let enabledClientsStr = "";
|
|
13
|
+
if (process.env.MCP_CLIENTS) {
|
|
14
|
+
enabledClientsStr = process.env.MCP_CLIENTS.trim();
|
|
15
|
+
}
|
|
16
|
+
enabledClientsStr += ",";
|
|
17
|
+
if (process.env.MCP_TOOLSETS) {
|
|
18
|
+
enabledClientsStr += process.env.MCP_TOOLSETS.trim();
|
|
16
19
|
}
|
|
17
20
|
this.enabledClients = new Set(
|
|
18
|
-
|
|
21
|
+
enabledClientsStr.trim().split(",").map((name) => {
|
|
22
|
+
if (name.includes(":")) {
|
|
23
|
+
return name.split(":")[0].trim().toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
return name.trim().toLowerCase();
|
|
26
|
+
}).filter((name) => name.length > 0)
|
|
19
27
|
);
|
|
20
28
|
}
|
|
21
29
|
/**
|
|
22
|
-
* Check if a client is enabled based on
|
|
30
|
+
* Check if a client is enabled based on client filtering configuration
|
|
23
31
|
*/
|
|
24
32
|
isClientEnabled(name) {
|
|
25
|
-
if (this.enabledClients ===
|
|
33
|
+
if (this.enabledClients.size === 0) {
|
|
26
34
|
return true;
|
|
27
35
|
}
|
|
28
36
|
return this.enabledClients.has(name.toLowerCase());
|
|
@@ -76,31 +84,57 @@ class ClientRegistry {
|
|
|
76
84
|
return this.entries.filter((entry) => this.isClientEnabled(entry.name));
|
|
77
85
|
}
|
|
78
86
|
/**
|
|
79
|
-
*
|
|
87
|
+
* Registers all enabled clients on the given MCP server
|
|
80
88
|
* @param server The MCP server on which the client is registered
|
|
81
89
|
* @param getConfigValue A function that obtains a configuration value for the given client and requirement name
|
|
82
|
-
* @returns The number of clients successfully
|
|
90
|
+
* @returns The number of clients successfully added
|
|
83
91
|
*/
|
|
84
|
-
async
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
async registerAll(server, getConfigValue, configure, authorizationCheck) {
|
|
93
|
+
if (authorizationCheck && !configure) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
"Cannot perform authorization check without configuring clients"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
let addedCount = 0;
|
|
99
|
+
clientLoop: for (const client of this.getAll()) {
|
|
100
|
+
if (configure) {
|
|
101
|
+
const config = {};
|
|
102
|
+
for (const configKey of Object.keys(client.config.shape)) {
|
|
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;
|
|
95
132
|
}
|
|
96
133
|
}
|
|
97
|
-
await
|
|
98
|
-
|
|
99
|
-
await server.addClient(entry);
|
|
100
|
-
configuredCount++;
|
|
101
|
-
}
|
|
134
|
+
await server.addClient(client);
|
|
135
|
+
addedCount++;
|
|
102
136
|
}
|
|
103
|
-
return
|
|
137
|
+
return addedCount;
|
|
104
138
|
}
|
|
105
139
|
/**
|
|
106
140
|
* Clear all registrations (useful for testing)
|
package/dist/common/server.js
CHANGED
|
@@ -11,7 +11,9 @@ class SmartBearMcpServer extends McpServer {
|
|
|
11
11
|
samplingSupported = false;
|
|
12
12
|
elicitationSupported = false;
|
|
13
13
|
clients = [];
|
|
14
|
-
|
|
14
|
+
enabledToolsets;
|
|
15
|
+
getEnvFn;
|
|
16
|
+
constructor(getEnvFn, enabledToolsets) {
|
|
15
17
|
super(
|
|
16
18
|
{
|
|
17
19
|
name: MCP_SERVER_NAME,
|
|
@@ -27,11 +29,26 @@ class SmartBearMcpServer extends McpServer {
|
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
);
|
|
32
|
+
this.getEnvFn = getEnvFn;
|
|
30
33
|
this.cache = new CacheService();
|
|
34
|
+
if (enabledToolsets) {
|
|
35
|
+
this.enabledToolsets = enabledToolsets.split(",").map((s) => s.trim().toLowerCase());
|
|
36
|
+
}
|
|
31
37
|
}
|
|
32
38
|
getCache() {
|
|
33
39
|
return this.cache;
|
|
34
40
|
}
|
|
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
|
+
};
|
|
35
52
|
setSamplingSupported(supported) {
|
|
36
53
|
this.samplingSupported = supported;
|
|
37
54
|
}
|
|
@@ -56,6 +73,9 @@ class SmartBearMcpServer extends McpServer {
|
|
|
56
73
|
this.clients.push(client);
|
|
57
74
|
await client.registerTools(
|
|
58
75
|
(params, cb) => {
|
|
76
|
+
if (!this.isToolEnabled(client, params.toolset)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
59
79
|
const toolName = this.getCapabilityName(client, params.title);
|
|
60
80
|
const toolTitle = this.getCapabilityTitle(client, params.title);
|
|
61
81
|
if (toolName.length > 64) {
|
|
@@ -236,9 +256,40 @@ ${result.instructions}`
|
|
|
236
256
|
getCapabilityName(client, title) {
|
|
237
257
|
return `${client.capabilityPrefix}_${title.replace(/\s+/g, "_").toLowerCase()}`;
|
|
238
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* The tool is enabled if:
|
|
261
|
+
* - No enabled toolsets are defined on the server, or
|
|
262
|
+
* - The client is included in the enabled toolsets list, or
|
|
263
|
+
* - The toolset is included in the enabled toolsets list, or
|
|
264
|
+
* - The toolset is in the client's default list and there is at least one specific toolset enabled for the client
|
|
265
|
+
* @param client
|
|
266
|
+
* @param toolset
|
|
267
|
+
* @returns whether to register the tool based on enabled toolsets configuration
|
|
268
|
+
*/
|
|
269
|
+
isToolEnabled(client, toolset) {
|
|
270
|
+
if (!this.enabledToolsets) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
const clientPrefix = client.configPrefix.toLowerCase();
|
|
274
|
+
const clientIsEnabled = this.enabledToolsets.some(
|
|
275
|
+
(ts) => !ts.includes(":") && ts === clientPrefix
|
|
276
|
+
);
|
|
277
|
+
if (clientIsEnabled) {
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
const toolsetEntries = this.enabledToolsets.filter(
|
|
281
|
+
(ts) => ts.includes(":") && ts.split(":")[0] === clientPrefix
|
|
282
|
+
);
|
|
283
|
+
if (toolsetEntries.length === 0) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
const toolsetName = `${clientPrefix}:${toolset.replace(/[\s\-_]/g, "")}`.toLowerCase();
|
|
287
|
+
return toolsetEntries.includes(toolsetName) || (client.defaultToolsets || [])?.includes(toolset);
|
|
288
|
+
}
|
|
239
289
|
getDescription(params) {
|
|
240
290
|
const {
|
|
241
291
|
summary,
|
|
292
|
+
toolset,
|
|
242
293
|
useCases,
|
|
243
294
|
examples,
|
|
244
295
|
inputSchema,
|
|
@@ -246,6 +297,11 @@ ${result.instructions}`
|
|
|
246
297
|
outputDescription
|
|
247
298
|
} = params;
|
|
248
299
|
let description = summary;
|
|
300
|
+
if (toolset) {
|
|
301
|
+
description += `
|
|
302
|
+
|
|
303
|
+
**Toolset:** ${toolset}`;
|
|
304
|
+
}
|
|
249
305
|
if (inputSchema && inputSchema instanceof ZodObject) {
|
|
250
306
|
let parameters = Object.keys(inputSchema.shape).map((key) => {
|
|
251
307
|
const field = inputSchema.shape[key];
|
|
@@ -5,11 +5,12 @@ 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";
|
|
9
8
|
import { SmartBearMcpServer } from "./server.js";
|
|
10
9
|
import { isDraining, registerShutdownHandler } from "./shutdown.js";
|
|
11
10
|
import { getEnvVarName } from "./transport-stdio.js";
|
|
12
|
-
import {
|
|
11
|
+
import { getTypeDescription, isOptionalType } from "./zod-utils.js";
|
|
12
|
+
class AuthorizationError extends Error {
|
|
13
|
+
}
|
|
13
14
|
const PROBE_HEADERS = {
|
|
14
15
|
"Content-Type": "application/json",
|
|
15
16
|
"Cache-Control": "no-store"
|
|
@@ -280,10 +281,7 @@ async function handleStreamableHttpRequest(req, res, transports) {
|
|
|
280
281
|
);
|
|
281
282
|
return;
|
|
282
283
|
}
|
|
283
|
-
await
|
|
284
|
-
req,
|
|
285
|
-
async () => await transport.handleRequest(req, res, parsedBody)
|
|
286
|
-
);
|
|
284
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
287
285
|
} catch (error) {
|
|
288
286
|
console.error("Error handling StreamableHTTP request:", error);
|
|
289
287
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
@@ -328,13 +326,10 @@ async function handleLegacyMessageRequest(req, res, url, transports) {
|
|
|
328
326
|
req.on("end", async () => {
|
|
329
327
|
try {
|
|
330
328
|
const parsedBody = JSON.parse(body);
|
|
331
|
-
await
|
|
329
|
+
await session.transport.handlePostMessage(
|
|
332
330
|
req,
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
res,
|
|
336
|
-
parsedBody
|
|
337
|
-
)
|
|
331
|
+
res,
|
|
332
|
+
parsedBody
|
|
338
333
|
);
|
|
339
334
|
} catch (error) {
|
|
340
335
|
console.error("Error handling POST message:", error);
|
|
@@ -343,93 +338,118 @@ async function handleLegacyMessageRequest(req, res, url, transports) {
|
|
|
343
338
|
}
|
|
344
339
|
});
|
|
345
340
|
}
|
|
341
|
+
function resolveFromRequest(req, key, prefix) {
|
|
342
|
+
const queryStringName = getQueryStringName(key, prefix);
|
|
343
|
+
const queryParams = querystring.parse(req.url?.split("?")[1] || "");
|
|
344
|
+
let value = queryParams[queryStringName] || queryParams[queryStringName.toLowerCase()];
|
|
345
|
+
if (typeof value === "string") {
|
|
346
|
+
return value;
|
|
347
|
+
}
|
|
348
|
+
const headerName = getHeaderName(key, prefix);
|
|
349
|
+
value = req.headers[headerName] || req.headers[headerName.toLowerCase()];
|
|
350
|
+
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
|
+
return value;
|
|
359
|
+
}
|
|
360
|
+
const envVarName = getEnvVarName(key, prefix);
|
|
361
|
+
return process.env[envVarName];
|
|
362
|
+
}
|
|
363
|
+
function makeConfigFn(req) {
|
|
364
|
+
return (key, client) => resolveFromRequest(req, key, client?.configPrefix);
|
|
365
|
+
}
|
|
346
366
|
async function newServer(req, res) {
|
|
347
|
-
const
|
|
367
|
+
const configFn = makeConfigFn(req);
|
|
368
|
+
const enabledToolsets = resolveFromRequest(req, "toolsets", "smartbear") || void 0;
|
|
369
|
+
const server = new SmartBearMcpServer(configFn, enabledToolsets);
|
|
348
370
|
try {
|
|
349
|
-
const configuredCount = await
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const queryStringName = getQueryStringName(client, key);
|
|
355
|
-
const queryParams = querystring.parse(req.url?.split("?")[1] || "");
|
|
356
|
-
let value = queryParams[queryStringName] || queryParams[queryStringName.toLowerCase()];
|
|
357
|
-
if (typeof value === "string") {
|
|
358
|
-
return value;
|
|
359
|
-
}
|
|
360
|
-
const headerName = getHeaderName(client, key);
|
|
361
|
-
value = req.headers[headerName] || req.headers[headerName.toLowerCase()];
|
|
362
|
-
if (typeof value === "string") {
|
|
363
|
-
return value;
|
|
364
|
-
}
|
|
365
|
-
const envVarName = getEnvVarName(client, key);
|
|
366
|
-
return process.env[envVarName] || null;
|
|
367
|
-
},
|
|
368
|
-
true
|
|
369
|
-
// ignoreMissingRequiredConfigs
|
|
370
|
-
)
|
|
371
|
-
);
|
|
372
|
-
console.log(
|
|
373
|
-
`Configured ${configuredCount} clients for new server instance`
|
|
371
|
+
const configuredCount = await clientRegistry.registerAll(
|
|
372
|
+
server,
|
|
373
|
+
configFn,
|
|
374
|
+
true,
|
|
375
|
+
false
|
|
374
376
|
);
|
|
375
377
|
if (configuredCount === 0) {
|
|
376
378
|
throw new Error(
|
|
377
|
-
"No clients successfully configured.
|
|
379
|
+
"No clients successfully configured. The request headers are missing the required configuration."
|
|
378
380
|
);
|
|
379
381
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
() => server.getClients().some((client) => {
|
|
383
|
-
if (!client.getAuthToken) return true;
|
|
384
|
-
return client.getAuthToken() !== null;
|
|
385
|
-
})
|
|
382
|
+
console.log(
|
|
383
|
+
`Configured ${configuredCount} clients for new server instance`
|
|
386
384
|
);
|
|
387
|
-
|
|
388
|
-
|
|
385
|
+
const hasNoAuth = !server.getClients().some((client) => client.hasAuth());
|
|
386
|
+
if (hasNoAuth) {
|
|
387
|
+
throw new AuthorizationError(
|
|
389
388
|
"No clients have valid authentication credentials. Please authenticate via OAuth or provide alternative auth headers (e.g. API key or personal auth token)."
|
|
390
389
|
);
|
|
391
390
|
}
|
|
391
|
+
return server;
|
|
392
392
|
} catch (error) {
|
|
393
|
-
const headerHelp = getHttpHeadersHelp();
|
|
394
|
-
const errorMessage = headerHelp.length > 0 ? `Configuration error: ${error instanceof Error ? error.message : String(error)}. Please provide valid headers:
|
|
395
|
-
${headerHelp.join("\n")}` : "No clients support HTTP header configuration.";
|
|
396
393
|
const headers = {
|
|
397
394
|
"Content-Type": "text/plain"
|
|
398
395
|
};
|
|
399
|
-
if (
|
|
400
|
-
|
|
396
|
+
if (error instanceof AuthorizationError) {
|
|
397
|
+
if (req.headers.host) {
|
|
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);
|
|
401
411
|
}
|
|
402
|
-
res.writeHead(401, headers);
|
|
403
|
-
res.end(errorMessage);
|
|
404
412
|
return null;
|
|
405
413
|
}
|
|
406
|
-
return server;
|
|
407
414
|
}
|
|
408
|
-
function getHeaderName(
|
|
409
|
-
|
|
415
|
+
function getHeaderName(key, clientPrefix) {
|
|
416
|
+
const prefix = `${clientPrefix ? `${clientPrefix}-${key}` : key}`;
|
|
417
|
+
return prefix.split(/[\s\-_]/).map(
|
|
410
418
|
(part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
|
|
411
|
-
).join("-")
|
|
419
|
+
).join("-");
|
|
412
420
|
}
|
|
413
|
-
function getQueryStringName(
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
421
|
+
function getQueryStringName(key, clientPrefix) {
|
|
422
|
+
const prefix = `${clientPrefix ? `${clientPrefix}-${key}` : key}`;
|
|
423
|
+
return prefix.split(/[\s\-_]/).map(
|
|
424
|
+
(part, i) => i === 0 ? part.toLowerCase() : part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()
|
|
425
|
+
).join("");
|
|
417
426
|
}
|
|
418
427
|
function getHttpHeaders() {
|
|
419
428
|
const headers = /* @__PURE__ */ new Set();
|
|
420
|
-
for (const
|
|
421
|
-
for (const
|
|
422
|
-
|
|
429
|
+
for (const client of clientRegistry.getAll()) {
|
|
430
|
+
for (const key of [
|
|
431
|
+
...Object.keys(client.config.shape),
|
|
432
|
+
...Object.keys(client.authenticationFields.shape)
|
|
433
|
+
]) {
|
|
434
|
+
headers.add(getHeaderName(key, client.configPrefix));
|
|
423
435
|
}
|
|
424
436
|
}
|
|
425
437
|
return Array.from(headers).sort((a, b) => a.localeCompare(b));
|
|
426
438
|
}
|
|
427
439
|
function getHttpHeadersHelp() {
|
|
428
440
|
const messages = [];
|
|
429
|
-
for (const
|
|
430
|
-
messages.push(` - ${
|
|
431
|
-
for (const [
|
|
432
|
-
|
|
441
|
+
for (const client of clientRegistry.getAll()) {
|
|
442
|
+
messages.push(` - ${client.name}:`);
|
|
443
|
+
for (const [authKey, requirement] of Object.entries(
|
|
444
|
+
client.authenticationFields.shape
|
|
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
453
|
const requiredTag = isOptionalType(requirement) ? " (optional)" : " (required)";
|
|
434
454
|
messages.push(
|
|
435
455
|
` - ${headerName}${requiredTag}: ${getTypeDescription(requirement)}`
|
|
@@ -439,6 +459,7 @@ function getHttpHeadersHelp() {
|
|
|
439
459
|
return messages;
|
|
440
460
|
}
|
|
441
461
|
export {
|
|
462
|
+
AuthorizationError,
|
|
442
463
|
drainHttpTransport,
|
|
443
464
|
getBaseUrl,
|
|
444
465
|
getHeaderName,
|
|
@@ -1,18 +1,20 @@
|
|
|
1
|
+
import { enableCompileCache } from "node:module";
|
|
1
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
3
|
import { clientRegistry } from "./client-registry.js";
|
|
3
4
|
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "./info.js";
|
|
4
5
|
import { SmartBearMcpServer } from "./server.js";
|
|
5
6
|
import { registerShutdownHandler } from "./shutdown.js";
|
|
6
|
-
import {
|
|
7
|
-
function
|
|
7
|
+
import { getTypeDescription } from "./zod-utils.js";
|
|
8
|
+
function getConfigMessage() {
|
|
8
9
|
const messages = [];
|
|
9
|
-
for (const
|
|
10
|
-
messages.push(` - ${
|
|
11
|
-
for (const [configKey, requirement] of
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
for (const client of clientRegistry.getAll()) {
|
|
11
|
+
messages.push(` - ${client.name}:`);
|
|
12
|
+
for (const [configKey, requirement] of [
|
|
13
|
+
...Object.entries(client.authenticationFields.shape),
|
|
14
|
+
...Object.entries(client.config.shape)
|
|
15
|
+
]) {
|
|
14
16
|
messages.push(
|
|
15
|
-
` - ${
|
|
17
|
+
` - ${getEnvVarName(configKey, client.configPrefix)}: ${getTypeDescription(requirement)}`
|
|
16
18
|
);
|
|
17
19
|
}
|
|
18
20
|
}
|
|
@@ -26,26 +28,28 @@ async function runStdioMode() {
|
|
|
26
28
|
console.log(
|
|
27
29
|
"The following environment variables can be set to configure each of the SmartBear clients:"
|
|
28
30
|
);
|
|
29
|
-
console.log(
|
|
31
|
+
console.log(getConfigMessage().join("\n"));
|
|
30
32
|
process.exit(0);
|
|
31
33
|
}
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
+
enableCompileCache();
|
|
35
|
+
const configFn = (key, client) => {
|
|
36
|
+
const envVarName = getEnvVarName(key, client?.configPrefix);
|
|
37
|
+
return process.env[envVarName];
|
|
38
|
+
};
|
|
39
|
+
const server = new SmartBearMcpServer(configFn, process.env.MCP_TOOLSETS);
|
|
40
|
+
const addedCount = await clientRegistry.registerAll(
|
|
34
41
|
server,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
42
|
+
configFn,
|
|
43
|
+
true,
|
|
44
|
+
true
|
|
39
45
|
);
|
|
40
|
-
if (
|
|
41
|
-
const message =
|
|
46
|
+
if (addedCount === 0) {
|
|
47
|
+
const message = getConfigMessage();
|
|
42
48
|
console.warn(
|
|
43
|
-
|
|
44
|
-
${message.join("\n")}`
|
|
49
|
+
`No clients configured. Please provide valid environment variables for at least one client:
|
|
50
|
+
${message.join("\n")}`
|
|
45
51
|
);
|
|
46
|
-
|
|
47
|
-
await server.addClient(entry);
|
|
48
|
-
}
|
|
52
|
+
await clientRegistry.registerAll(server, configFn, false, false);
|
|
49
53
|
}
|
|
50
54
|
const transport = new StdioServerTransport();
|
|
51
55
|
registerShutdownHandler("stdio-transport", async () => {
|
|
@@ -70,8 +74,9 @@ ${message.join("\n")}` : "No clients support environment variable configuration.
|
|
|
70
74
|
};
|
|
71
75
|
await server.connect(transport);
|
|
72
76
|
}
|
|
73
|
-
function getEnvVarName(
|
|
74
|
-
|
|
77
|
+
function getEnvVarName(key, clientPrefix) {
|
|
78
|
+
const prefix = `${clientPrefix ? `${clientPrefix}-${key}` : key}`.toUpperCase();
|
|
79
|
+
return prefix.replace(/[\s\-_]/g, "_");
|
|
75
80
|
}
|
|
76
81
|
export {
|
|
77
82
|
getEnvVarName,
|
package/dist/package.json.js
CHANGED
|
@@ -308,7 +308,7 @@ const AdminUserIdSchema = z.object({
|
|
|
308
308
|
userId: z.string().describe("UUID of the user")
|
|
309
309
|
});
|
|
310
310
|
const CreateAdminUserSchema = z.object({
|
|
311
|
-
email: z.string().
|
|
311
|
+
email: z.string().describe("Email address of the new user"),
|
|
312
312
|
name: z.string().describe("Display name of the new user"),
|
|
313
313
|
firstName: z.string().optional().describe("First name"),
|
|
314
314
|
lastName: z.string().optional().describe("Last name"),
|
|
@@ -318,7 +318,7 @@ const CreateAdminUserSchema = z.object({
|
|
|
318
318
|
const UpdateAdminUserSchema = z.object({
|
|
319
319
|
userId: z.string().describe("UUID of the user to update"),
|
|
320
320
|
active: z.boolean().optional().describe("Whether the user is active"),
|
|
321
|
-
email: z.string().
|
|
321
|
+
email: z.string().optional().describe("New email address"),
|
|
322
322
|
firstName: z.string().optional().describe("First name"),
|
|
323
323
|
lastName: z.string().optional().describe("Last name"),
|
|
324
324
|
name: z.string().optional().describe("Display name")
|
|
@@ -326,7 +326,7 @@ const UpdateAdminUserSchema = z.object({
|
|
|
326
326
|
const InviteUsersSchema = z.object({
|
|
327
327
|
users: z.array(
|
|
328
328
|
z.object({
|
|
329
|
-
email: z.string().
|
|
329
|
+
email: z.string().describe("Email address"),
|
|
330
330
|
name: z.string().min(1).describe("Display name")
|
|
331
331
|
})
|
|
332
332
|
).min(1).describe("List of users to invite")
|