@smartbear/mcp 0.10.0 → 0.11.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 -8
- package/dist/bugsnag/client/api/index.js +2 -0
- package/dist/bugsnag/client/filters.js +0 -6
- package/dist/bugsnag/client.js +234 -376
- package/dist/bugsnag/input-schemas.js +51 -0
- package/dist/collaborator/client.js +18 -5
- package/dist/common/cache.js +63 -0
- package/dist/common/client-registry.js +128 -0
- package/dist/common/register-clients.js +31 -0
- package/dist/common/server.js +30 -9
- package/dist/common/transport-http.js +377 -0
- package/dist/common/transport-stdio.js +43 -0
- package/dist/index.js +20 -70
- package/dist/pactflow/client.js +39 -19
- package/dist/qmetry/client.js +24 -9
- package/dist/reflect/client.js +10 -4
- package/dist/{api-hub → swagger}/client/api.js +2 -2
- package/dist/{api-hub → swagger}/client/configuration.js +1 -1
- package/dist/{api-hub → swagger}/client/index.js +2 -2
- package/dist/{api-hub → swagger}/client/tools.js +4 -4
- package/dist/{api-hub → swagger}/client.js +47 -35
- package/dist/swagger/config-utils.js +18 -0
- package/dist/zephyr/client.js +40 -8
- package/dist/zephyr/common/rest-api-schemas.js +79 -78
- package/dist/zephyr/tool/priority/get-priorities.js +43 -0
- package/dist/zephyr/tool/status/get-statuses.js +49 -0
- package/dist/zephyr/tool/test-case/get-test-case.js +39 -0
- package/dist/zephyr/tool/test-case/get-test-cases.js +64 -0
- package/dist/zephyr/tool/test-cycle/get-test-cycle.js +39 -0
- package/dist/zephyr/tool/test-cycle/get-test-cycles.js +2 -2
- package/dist/zephyr/tool/test-execution/get-test-execution.js +39 -0
- package/package.json +2 -2
- /package/dist/{api-hub → swagger}/client/portal-types.js +0 -0
- /package/dist/{api-hub → swagger}/client/registry-types.js +0 -0
- /package/dist/{api-hub → swagger}/client/user-management-types.js +0 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
4
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { clientRegistry } from "./client-registry.js";
|
|
7
|
+
import { SmartBearMcpServer } from "./server.js";
|
|
8
|
+
/**
|
|
9
|
+
* Run server in HTTP mode with Streamable HTTP transport
|
|
10
|
+
* Supports both SSE (legacy) and StreamableHTTP transports for backwards compatibility
|
|
11
|
+
*/
|
|
12
|
+
export async function runHttpMode() {
|
|
13
|
+
const PORT = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 3000;
|
|
14
|
+
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",") || [
|
|
15
|
+
"http://localhost:3000",
|
|
16
|
+
];
|
|
17
|
+
// Store transports by session ID
|
|
18
|
+
const transports = new Map();
|
|
19
|
+
// Get dynamic list of allowed headers from registered clients
|
|
20
|
+
const allowedAuthHeaders = getHttpHeaders();
|
|
21
|
+
const allowedHeaders = [
|
|
22
|
+
"Content-Type",
|
|
23
|
+
"Authorization",
|
|
24
|
+
"MCP-Session-Id", // Required for StreamableHTTP
|
|
25
|
+
"x-custom-auth-headers", // used by mcp-inspector
|
|
26
|
+
...allowedAuthHeaders,
|
|
27
|
+
].join(", ");
|
|
28
|
+
const httpServer = createServer(async (req, res) => {
|
|
29
|
+
// Enable CORS
|
|
30
|
+
const origin = req.headers.origin || "";
|
|
31
|
+
if (allowedOrigins.includes(origin)) {
|
|
32
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
33
|
+
}
|
|
34
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
35
|
+
res.setHeader("Access-Control-Allow-Headers", allowedHeaders);
|
|
36
|
+
res.setHeader("Access-Control-Expose-Headers", "MCP-Session-Id");
|
|
37
|
+
if (req.method === "OPTIONS") {
|
|
38
|
+
res.writeHead(200);
|
|
39
|
+
res.end();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
43
|
+
// HEALTH CHECK ENDPOINT
|
|
44
|
+
if (req.method === "GET" && url.pathname === "/health") {
|
|
45
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
46
|
+
res.end(JSON.stringify({ status: "ok", timestamp: new Date().toISOString() }));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// STREAMABLE HTTP ENDPOINT (modern, preferred)
|
|
50
|
+
if (url.pathname === "/mcp") {
|
|
51
|
+
await handleStreamableHttpRequest(req, res, transports);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// LEGACY SSE ENDPOINT (for backwards compatibility)
|
|
55
|
+
if (req.method === "GET" && url.pathname === "/sse") {
|
|
56
|
+
await handleLegacySseRequest(req, res, transports);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (req.method === "POST" && url.pathname === "/message") {
|
|
60
|
+
await handleLegacyMessageRequest(req, res, url, transports);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
64
|
+
res.end("Not found");
|
|
65
|
+
});
|
|
66
|
+
httpServer.listen(PORT, () => {
|
|
67
|
+
console.log(`[MCP HTTP Server] Listening on http://localhost:${PORT}`);
|
|
68
|
+
console.log(`[MCP HTTP Server] Health check: http://localhost:${PORT}/health`);
|
|
69
|
+
console.log(`[MCP HTTP Server] Modern endpoint: http://localhost:${PORT}/mcp (Streamable HTTP)`);
|
|
70
|
+
console.log(`[MCP HTTP Server] Legacy endpoint: http://localhost:${PORT}/sse (SSE)`);
|
|
71
|
+
const headerHelp = getHttpHeadersHelp();
|
|
72
|
+
if (headerHelp.length > 0) {
|
|
73
|
+
console.log(`[MCP HTTP Server] Send configuration headers:\n${headerHelp.join("\n")}`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
console.warn(`[MCP HTTP Server] No clients support HTTP header configuration`);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Parse request body for POST requests
|
|
82
|
+
* Reads the request stream and parses it as JSON
|
|
83
|
+
* @returns Parsed JSON object or undefined if not a POST request or parsing fails
|
|
84
|
+
*/
|
|
85
|
+
async function parseRequestBody(req) {
|
|
86
|
+
if (req.method !== "POST") {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
let body = "";
|
|
90
|
+
req.on("data", (chunk) => {
|
|
91
|
+
body += chunk.toString();
|
|
92
|
+
});
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
req.on("end", () => {
|
|
95
|
+
try {
|
|
96
|
+
resolve(JSON.parse(body));
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
console.error("Error parsing request body:", error);
|
|
100
|
+
resolve(undefined);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get existing transport for session or return error response
|
|
107
|
+
* Validates that the session exists and uses StreamableHTTP transport
|
|
108
|
+
* @returns StreamableHTTPServerTransport if valid, null otherwise (with error response sent)
|
|
109
|
+
*/
|
|
110
|
+
function getExistingTransport(sessionId, transports, res) {
|
|
111
|
+
const existing = transports.get(sessionId);
|
|
112
|
+
if (existing && existing.transport instanceof StreamableHTTPServerTransport) {
|
|
113
|
+
return existing.transport;
|
|
114
|
+
}
|
|
115
|
+
// Session doesn't exist or is using a different transport (e.g., SSE)
|
|
116
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
117
|
+
res.end(JSON.stringify({
|
|
118
|
+
jsonrpc: "2.0",
|
|
119
|
+
error: {
|
|
120
|
+
code: -32000,
|
|
121
|
+
message: "Bad Request: Session exists but uses a different transport protocol",
|
|
122
|
+
},
|
|
123
|
+
id: null,
|
|
124
|
+
}));
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Create new transport for initialize request
|
|
129
|
+
* Sets up a new MCP server instance with configuration from HTTP headers,
|
|
130
|
+
* creates a StreamableHTTP transport, and registers session lifecycle handlers
|
|
131
|
+
* @returns StreamableHTTPServerTransport if successful, null if server initialization fails
|
|
132
|
+
*/
|
|
133
|
+
async function createNewTransport(req, res, transports) {
|
|
134
|
+
// Create and configure server with headers from the request
|
|
135
|
+
const server = await newServer(req, res);
|
|
136
|
+
if (!server) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
// Create transport with session management
|
|
140
|
+
const transport = new StreamableHTTPServerTransport({
|
|
141
|
+
sessionIdGenerator: () => randomUUID(),
|
|
142
|
+
onsessioninitialized: (newSessionId) => {
|
|
143
|
+
console.log(`[MCP] New session initialized: ${newSessionId}`);
|
|
144
|
+
// Store session so subsequent requests can find it
|
|
145
|
+
transports.set(newSessionId, { server, transport });
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
// Clean up session on close
|
|
149
|
+
transport.onclose = () => {
|
|
150
|
+
if (transport.sessionId) {
|
|
151
|
+
console.log(`[MCP] Session closed: ${transport.sessionId}`);
|
|
152
|
+
transports.delete(transport.sessionId);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
// Connect server to transport to start handling messages
|
|
156
|
+
await server.connect(transport);
|
|
157
|
+
return transport;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Handle modern Streamable HTTP requests
|
|
161
|
+
* This is the main endpoint (/mcp) for the modern MCP StreamableHTTP transport.
|
|
162
|
+
*
|
|
163
|
+
* Request flow:
|
|
164
|
+
* 1. First request (initialize): No session ID, body contains initialize request
|
|
165
|
+
* - Creates new server + transport, generates session ID
|
|
166
|
+
* 2. Subsequent requests: Include MCP-Session-Id header
|
|
167
|
+
* - Routes to existing transport for the session
|
|
168
|
+
*/
|
|
169
|
+
async function handleStreamableHttpRequest(req, res, transports) {
|
|
170
|
+
try {
|
|
171
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
172
|
+
const parsedBody = await parseRequestBody(req);
|
|
173
|
+
let transport;
|
|
174
|
+
// Case 1: Existing session - route to existing transport
|
|
175
|
+
if (sessionId && transports.has(sessionId)) {
|
|
176
|
+
const existingTransport = getExistingTransport(sessionId, transports, res);
|
|
177
|
+
if (!existingTransport)
|
|
178
|
+
return;
|
|
179
|
+
transport = existingTransport;
|
|
180
|
+
}
|
|
181
|
+
// Case 2: New session - must be an initialize request
|
|
182
|
+
else if (!sessionId &&
|
|
183
|
+
req.method === "POST" &&
|
|
184
|
+
parsedBody &&
|
|
185
|
+
isInitializeRequest(parsedBody)) {
|
|
186
|
+
const newTransport = await createNewTransport(req, res, transports);
|
|
187
|
+
if (!newTransport)
|
|
188
|
+
return;
|
|
189
|
+
transport = newTransport;
|
|
190
|
+
}
|
|
191
|
+
// Case 3: Invalid request
|
|
192
|
+
else {
|
|
193
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
194
|
+
res.end(JSON.stringify({
|
|
195
|
+
jsonrpc: "2.0",
|
|
196
|
+
error: {
|
|
197
|
+
code: -32000,
|
|
198
|
+
message: "Bad Request: Invalid request",
|
|
199
|
+
},
|
|
200
|
+
id: null,
|
|
201
|
+
}));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Delegate to transport to handle the MCP protocol message
|
|
205
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
console.error("Error handling StreamableHTTP request:", error);
|
|
209
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
210
|
+
res.end("Internal server error");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Handle legacy SSE connection requests (GET /sse)
|
|
215
|
+
*
|
|
216
|
+
* SSE (Server-Sent Events) transport maintains a long-lived connection
|
|
217
|
+
* for server-to-client messages, with a separate POST endpoint for client-to-server.
|
|
218
|
+
*
|
|
219
|
+
* This is kept for backwards compatibility with older MCP clients.
|
|
220
|
+
* New integrations should use the modern StreamableHTTP transport (/mcp).
|
|
221
|
+
*/
|
|
222
|
+
async function handleLegacySseRequest(req, res, transports) {
|
|
223
|
+
// Create a new server instance for this connection
|
|
224
|
+
const server = await newServer(req, res);
|
|
225
|
+
if (!server) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// SSE transport keeps the connection open and sends events to the client
|
|
229
|
+
const transport = new SSEServerTransport("/message", res);
|
|
230
|
+
// Store the session so POST /message requests can find it
|
|
231
|
+
transports.set(transport.sessionId, { server, transport });
|
|
232
|
+
// Clean up session when connection closes
|
|
233
|
+
res.on("close", () => {
|
|
234
|
+
transports.delete(transport.sessionId);
|
|
235
|
+
});
|
|
236
|
+
// Connect server to transport (this also starts the transport automatically)
|
|
237
|
+
await server.connect(transport);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Handle legacy POST message requests (POST /message?sessionId=xxx)
|
|
241
|
+
*
|
|
242
|
+
* This endpoint is part of the legacy SSE transport, handling client-to-server messages.
|
|
243
|
+
* The SSE transport uses:
|
|
244
|
+
* - GET /sse: Server-to-client events (long-lived connection)
|
|
245
|
+
* - POST /message: Client-to-server messages (individual requests)
|
|
246
|
+
*
|
|
247
|
+
* New integrations should use the modern StreamableHTTP transport (/mcp).
|
|
248
|
+
*/
|
|
249
|
+
async function handleLegacyMessageRequest(req, res, url, transports) {
|
|
250
|
+
// Extract session ID from query parameter
|
|
251
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
252
|
+
if (!sessionId) {
|
|
253
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
254
|
+
res.end("Missing sessionId parameter");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// Find the session created by the SSE connection
|
|
258
|
+
const session = transports.get(sessionId);
|
|
259
|
+
if (!session) {
|
|
260
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
261
|
+
res.end("Session not found");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
// Validate this session is using SSE transport
|
|
265
|
+
if (!(session.transport instanceof SSEServerTransport)) {
|
|
266
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
267
|
+
res.end("Invalid transport for this endpoint");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
// Read and parse the request body
|
|
271
|
+
let body = "";
|
|
272
|
+
req.on("data", (chunk) => {
|
|
273
|
+
body += chunk.toString();
|
|
274
|
+
});
|
|
275
|
+
req.on("end", async () => {
|
|
276
|
+
try {
|
|
277
|
+
const parsedBody = JSON.parse(body);
|
|
278
|
+
// Route message to the SSE transport for processing
|
|
279
|
+
await session.transport.handlePostMessage(req, res, parsedBody);
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
console.error("Error handling POST message:", error);
|
|
283
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
284
|
+
res.end("Internal server error");
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Create a new MCP server instance with configuration from HTTP headers
|
|
290
|
+
*
|
|
291
|
+
* Configuration is read from HTTP headers in the format:
|
|
292
|
+
* {ClientPrefix}-{Field-Name} (e.g., Bugsnag-Auth-Token, Reflect-Api-Token)
|
|
293
|
+
*
|
|
294
|
+
* The ClientRegistry validates the configuration and initializes enabled clients.
|
|
295
|
+
* If configuration fails, an error response is sent and null is returned.
|
|
296
|
+
*
|
|
297
|
+
* @returns SmartBearMcpServer instance if successful, null if configuration fails
|
|
298
|
+
*/
|
|
299
|
+
async function newServer(req, res) {
|
|
300
|
+
const server = new SmartBearMcpServer();
|
|
301
|
+
try {
|
|
302
|
+
// Configure server with values from HTTP headers
|
|
303
|
+
await clientRegistry.configure(server, (client, key) => {
|
|
304
|
+
const headerName = getHeaderName(client, key);
|
|
305
|
+
// Check both original case and lower-case headers for compatibility
|
|
306
|
+
// (HTTP headers are case-insensitive, but Node.js lowercases them)
|
|
307
|
+
const value = req.headers[headerName] || req.headers[headerName.toLowerCase()];
|
|
308
|
+
if (typeof value === "string") {
|
|
309
|
+
return value;
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
// Configuration failed - provide helpful error message
|
|
316
|
+
const headerHelp = getHttpHeadersHelp();
|
|
317
|
+
const errorMessage = headerHelp.length > 0
|
|
318
|
+
? `Configuration error: ${error instanceof Error ? error.message : String(error)}. Please provide valid headers:\n${headerHelp.join("\n")}`
|
|
319
|
+
: "No clients support HTTP header configuration.";
|
|
320
|
+
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
321
|
+
res.end(errorMessage);
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
return server;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Convert a config key to HTTP header name format
|
|
328
|
+
*
|
|
329
|
+
* Examples:
|
|
330
|
+
* - auth_token -> Auth-Token
|
|
331
|
+
* - project_api_key -> Project-Api-Key
|
|
332
|
+
* - base_url -> Base-Url
|
|
333
|
+
*
|
|
334
|
+
* Combined with configPrefix: Bugsnag-Auth-Token, Reflect-Api-Token, etc.
|
|
335
|
+
*
|
|
336
|
+
* @param client The client instance (provides configPrefix)
|
|
337
|
+
* @param key The config key in snake_case
|
|
338
|
+
* @returns Header name in format: {ConfigPrefix}-{Pascal-Kebab-Case}
|
|
339
|
+
*/
|
|
340
|
+
function getHeaderName(client, key) {
|
|
341
|
+
return `${client.configPrefix}-${key
|
|
342
|
+
.split("_")
|
|
343
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
344
|
+
.join("-")}`;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Get all HTTP headers that clients support for authentication
|
|
348
|
+
* Returns a list of header names (in kebab-case) that should be allowed
|
|
349
|
+
*/
|
|
350
|
+
function getHttpHeaders() {
|
|
351
|
+
const headers = new Set();
|
|
352
|
+
// Use getAll() to respect MCP_ENABLED_CLIENTS filtering
|
|
353
|
+
for (const entry of clientRegistry.getAll()) {
|
|
354
|
+
for (const configKey of Object.keys(entry.config.shape)) {
|
|
355
|
+
headers.add(getHeaderName(entry, configKey));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return Array.from(headers).sort((a, b) => a.localeCompare(b));
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Get human-readable list of HTTP headers for logging/error messages
|
|
362
|
+
* Organized by client
|
|
363
|
+
*/
|
|
364
|
+
function getHttpHeadersHelp() {
|
|
365
|
+
const messages = [];
|
|
366
|
+
for (const entry of clientRegistry.getAll()) {
|
|
367
|
+
messages.push(` - ${entry.name}:`);
|
|
368
|
+
for (const [configKey, requirement] of Object.entries(entry.config.shape)) {
|
|
369
|
+
const headerName = getHeaderName(entry, configKey);
|
|
370
|
+
const requiredTag = requirement.isOptional()
|
|
371
|
+
? " (optional)"
|
|
372
|
+
: " (required)";
|
|
373
|
+
messages.push(` - ${headerName}${requiredTag}: ${requirement.description}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return messages;
|
|
377
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { clientRegistry } from "./client-registry.js";
|
|
3
|
+
import { SmartBearMcpServer } from "./server.js";
|
|
4
|
+
/**
|
|
5
|
+
* Generate a dynamic error message listing all available clients and their required env vars
|
|
6
|
+
*/
|
|
7
|
+
function getNoConfigErrorMessage() {
|
|
8
|
+
const messages = [];
|
|
9
|
+
for (const entry of clientRegistry.getAll()) {
|
|
10
|
+
messages.push(` - ${entry.name}:`);
|
|
11
|
+
for (const [configKey, requirement] of Object.entries(entry.config.shape)) {
|
|
12
|
+
const envVarName = getEnvVarName(entry, configKey);
|
|
13
|
+
const requiredTag = requirement.isOptional()
|
|
14
|
+
? " (optional)"
|
|
15
|
+
: " (required)";
|
|
16
|
+
messages.push(` - ${envVarName}${requiredTag}: ${requirement.description}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return messages;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Run server in STDIO mode (default)
|
|
23
|
+
*/
|
|
24
|
+
export async function runStdioMode() {
|
|
25
|
+
const server = new SmartBearMcpServer();
|
|
26
|
+
// Setup clients from environment variables
|
|
27
|
+
const configuredCount = await clientRegistry.configure(server, (client, key) => {
|
|
28
|
+
const envVarName = getEnvVarName(client, key);
|
|
29
|
+
return process.env[envVarName] || null;
|
|
30
|
+
});
|
|
31
|
+
if (configuredCount === 0) {
|
|
32
|
+
const errorMessage = getNoConfigErrorMessage();
|
|
33
|
+
console.error(errorMessage.length > 0
|
|
34
|
+
? `No clients configured. Please provide valid environment variables for at least one client:\n${errorMessage.join("\n")}`
|
|
35
|
+
: "No clients support environment variable configuration.");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
const transport = new StdioServerTransport();
|
|
39
|
+
await server.connect(transport);
|
|
40
|
+
}
|
|
41
|
+
function getEnvVarName(client, key) {
|
|
42
|
+
return `${client.configPrefix.toUpperCase().replace(/-/g, "_")}_${key.toUpperCase()}`;
|
|
43
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import { ApiHubClient } from "./api-hub/client.js";
|
|
4
|
-
import { BugsnagClient } from "./bugsnag/client.js";
|
|
5
|
-
import { CollaboratorClient } from "./collaborator/client.js";
|
|
6
2
|
import Bugsnag from "./common/bugsnag.js";
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { ReflectClient } from "./reflect/client.js";
|
|
11
|
-
import { ZephyrClient } from "./zephyr/client.js";
|
|
3
|
+
import "./common/register-clients.js"; // Register all available clients
|
|
4
|
+
import { runHttpMode } from "./common/transport-http.js";
|
|
5
|
+
import { runStdioMode } from "./common/transport-stdio.js";
|
|
12
6
|
// This is used to report errors in the MCP server itself
|
|
13
7
|
// If you want to use your own BugSnag API key, set the MCP_SERVER_BUGSNAG_API_KEY environment variable
|
|
14
8
|
const McpServerBugsnagAPIKey = process.env.MCP_SERVER_BUGSNAG_API_KEY;
|
|
@@ -16,69 +10,25 @@ if (McpServerBugsnagAPIKey) {
|
|
|
16
10
|
Bugsnag.start(McpServerBugsnagAPIKey);
|
|
17
11
|
}
|
|
18
12
|
async function main() {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const collaboratorBaseUrl = process.env.COLLAB_BASE_URL;
|
|
32
|
-
const collaboratorUsername = process.env.COLLAB_USERNAME;
|
|
33
|
-
const collaboratorLoginTicket = process.env.COLLAB_LOGIN_TICKET;
|
|
34
|
-
let client_defined = false;
|
|
35
|
-
if (reflectToken) {
|
|
36
|
-
server.addClient(new ReflectClient(reflectToken));
|
|
37
|
-
client_defined = true;
|
|
38
|
-
}
|
|
39
|
-
if (bugsnagToken) {
|
|
40
|
-
const bugsnagClient = new BugsnagClient(bugsnagToken, process.env.BUGSNAG_PROJECT_API_KEY, process.env.BUGSNAG_ENDPOINT);
|
|
41
|
-
await bugsnagClient.initialize();
|
|
42
|
-
server.addClient(bugsnagClient);
|
|
43
|
-
client_defined = true;
|
|
44
|
-
}
|
|
45
|
-
if (apiHubToken) {
|
|
46
|
-
server.addClient(new ApiHubClient(apiHubToken));
|
|
47
|
-
client_defined = true;
|
|
48
|
-
}
|
|
49
|
-
if (pactBrokerUrl) {
|
|
50
|
-
if (pactBrokerToken) {
|
|
51
|
-
server.addClient(new PactflowClient(pactBrokerToken, pactBrokerUrl, "pactflow", server.server));
|
|
52
|
-
client_defined = true;
|
|
53
|
-
}
|
|
54
|
-
else if (pactBrokerUsername && pactBrokerPassword) {
|
|
55
|
-
server.addClient(new PactflowClient({ username: pactBrokerUsername, password: pactBrokerPassword }, pactBrokerUrl, "pact_broker", server.server));
|
|
56
|
-
client_defined = true;
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
console.error("If the Pact Broker base URL is specified, you must specify either (a) a PactFlow token, or (b) a Pact Broker username and password pair.");
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (qmetryToken) {
|
|
63
|
-
server.addClient(new QmetryClient(qmetryToken, qmetryBaseUrl));
|
|
64
|
-
client_defined = true;
|
|
65
|
-
}
|
|
66
|
-
if (zephyrToken) {
|
|
67
|
-
server.addClient(new ZephyrClient(zephyrToken, zephyrBaseUrl));
|
|
68
|
-
client_defined = true;
|
|
69
|
-
}
|
|
70
|
-
if (collaboratorBaseUrl && collaboratorUsername && collaboratorLoginTicket) {
|
|
71
|
-
server.addClient(new CollaboratorClient(collaboratorBaseUrl, collaboratorUsername, collaboratorLoginTicket));
|
|
72
|
-
client_defined = true;
|
|
73
|
-
}
|
|
74
|
-
if (!client_defined) {
|
|
75
|
-
console.error("No SmartBear products were configured. Please provide at least one of the required configuration options.");
|
|
13
|
+
// Determine transport mode from environment variable
|
|
14
|
+
// MCP_TRANSPORT can be "stdio" (default) or "http"
|
|
15
|
+
const transportMode = process.env.MCP_TRANSPORT?.toLowerCase() || "stdio";
|
|
16
|
+
if (transportMode === "http") {
|
|
17
|
+
console.log("[MCP] Starting in HTTP mode...");
|
|
18
|
+
await runHttpMode();
|
|
19
|
+
}
|
|
20
|
+
else if (transportMode === "stdio") {
|
|
21
|
+
await runStdioMode();
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.error(`[MCP] Invalid transport mode: ${transportMode}. Use "stdio" or "http".`);
|
|
76
25
|
process.exit(1);
|
|
77
26
|
}
|
|
78
|
-
const transport = new StdioServerTransport();
|
|
79
|
-
await server.connect(transport);
|
|
80
27
|
}
|
|
81
|
-
|
|
28
|
+
try {
|
|
29
|
+
await main();
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
82
32
|
console.error("Fatal error in main():", error);
|
|
83
33
|
process.exit(1);
|
|
84
|
-
}
|
|
34
|
+
}
|
package/dist/pactflow/client.js
CHANGED
|
@@ -1,46 +1,66 @@
|
|
|
1
|
+
import z from "zod";
|
|
1
2
|
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
|
|
2
3
|
import { ToolError, } from "../common/types.js";
|
|
3
4
|
import { getOADMatcherRecommendations, getUserMatcherSelection, } from "./client/prompt-utils.js";
|
|
4
5
|
import { PROMPTS } from "./client/prompts.js";
|
|
5
6
|
import { TOOLS } from "./client/tools.js";
|
|
7
|
+
const ConfigurationSchema = z.object({
|
|
8
|
+
base_url: z.string().url().describe("Pact Broker or PactFlow base URL"),
|
|
9
|
+
token: z
|
|
10
|
+
.string()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe("Bearer token for PactFlow authentication (use this OR username/password)"),
|
|
13
|
+
username: z.string().optional().describe("Username for Pact Broker"),
|
|
14
|
+
password: z.string().optional().describe("Password for Pact Broker"),
|
|
15
|
+
});
|
|
6
16
|
// Tool definitions for PactFlow AI API client
|
|
7
17
|
export class PactflowClient {
|
|
8
18
|
name = "Contract Testing";
|
|
9
|
-
|
|
19
|
+
toolPrefix = "contract-testing";
|
|
20
|
+
configPrefix = "Pact-Broker";
|
|
21
|
+
config = ConfigurationSchema;
|
|
10
22
|
headers;
|
|
11
23
|
aiBaseUrl;
|
|
12
24
|
baseUrl;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
_clientType;
|
|
26
|
+
_server;
|
|
27
|
+
get server() {
|
|
28
|
+
if (!this._server)
|
|
29
|
+
throw new Error("Server not configured");
|
|
30
|
+
return this._server;
|
|
31
|
+
}
|
|
32
|
+
get clientType() {
|
|
33
|
+
if (!this._clientType)
|
|
34
|
+
throw new Error("Client not configured");
|
|
35
|
+
return this._clientType;
|
|
36
|
+
}
|
|
37
|
+
async configure(server, config) {
|
|
24
38
|
// Set headers based on the type of auth provided
|
|
25
|
-
if (typeof
|
|
39
|
+
if (typeof config.token === "string") {
|
|
26
40
|
this.headers = {
|
|
27
|
-
Authorization: `Bearer ${
|
|
41
|
+
Authorization: `Bearer ${config.token}`,
|
|
28
42
|
"Content-Type": "application/json",
|
|
29
43
|
"User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`,
|
|
30
44
|
};
|
|
45
|
+
this._clientType = "pactflow";
|
|
31
46
|
}
|
|
32
|
-
else
|
|
33
|
-
|
|
47
|
+
else if (typeof config.username === "string" &&
|
|
48
|
+
typeof config.password === "string") {
|
|
49
|
+
const authString = `${config.username}:${config.password}`;
|
|
34
50
|
this.headers = {
|
|
35
51
|
Authorization: `Basic ${Buffer.from(authString).toString("base64")}`,
|
|
36
52
|
"Content-Type": "application/json",
|
|
37
53
|
"User-Agent": `${MCP_SERVER_NAME}/${MCP_SERVER_VERSION}`,
|
|
38
54
|
};
|
|
55
|
+
this._clientType = "pact_broker";
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
return false; // Don't configure the client if no auth is provided
|
|
39
59
|
}
|
|
40
|
-
this.baseUrl =
|
|
60
|
+
this.baseUrl = config.base_url;
|
|
41
61
|
this.aiBaseUrl = `${this.baseUrl}/api/ai`;
|
|
42
|
-
this.
|
|
43
|
-
|
|
62
|
+
this._server = server.server;
|
|
63
|
+
return true;
|
|
44
64
|
}
|
|
45
65
|
// PactFlow AI client methods
|
|
46
66
|
/**
|
package/dist/qmetry/client.js
CHANGED
|
@@ -1,20 +1,35 @@
|
|
|
1
|
+
import z from "zod";
|
|
1
2
|
import { autoResolveViewIdAndFolderPath, findAutoResolveConfig, } from "./client/auto-resolve.js";
|
|
2
3
|
import { QMETRY_HANDLER_MAP } from "./client/handlers.js";
|
|
3
4
|
import { getProjectInfo } from "./client/project.js";
|
|
4
5
|
import { TOOLS } from "./client/tools/index.js";
|
|
5
6
|
import { QMETRY_DEFAULTS } from "./config/constants.js";
|
|
7
|
+
const ConfigurationSchema = z.object({
|
|
8
|
+
api_key: z.string().describe("QMetry API key for authentication"),
|
|
9
|
+
base_url: z
|
|
10
|
+
.string()
|
|
11
|
+
.url()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe("Optional QMetry base URL for custom or region-specific endpoints"),
|
|
14
|
+
});
|
|
6
15
|
export class QmetryClient {
|
|
7
16
|
name = "QMetry";
|
|
8
|
-
|
|
17
|
+
toolPrefix = "qmetry";
|
|
18
|
+
configPrefix = "Qmetry";
|
|
19
|
+
config = ConfigurationSchema;
|
|
9
20
|
token;
|
|
10
|
-
projectApiKey;
|
|
11
|
-
endpoint;
|
|
12
|
-
|
|
13
|
-
this.token =
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
projectApiKey = QMETRY_DEFAULTS.PROJECT_KEY;
|
|
22
|
+
endpoint = QMETRY_DEFAULTS.BASE_URL;
|
|
23
|
+
async configure(_server, config, _cache) {
|
|
24
|
+
this.token = config.api_key;
|
|
25
|
+
if (config.base_url) {
|
|
26
|
+
this.endpoint = config.base_url;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
16
29
|
}
|
|
17
30
|
getToken() {
|
|
31
|
+
if (!this.token)
|
|
32
|
+
throw new Error("Client not configured");
|
|
18
33
|
return this.token;
|
|
19
34
|
}
|
|
20
35
|
getBaseUrl() {
|
|
@@ -70,7 +85,7 @@ export class QmetryClient {
|
|
|
70
85
|
needsParentFolderIdAutoResolve) {
|
|
71
86
|
let projectInfo;
|
|
72
87
|
try {
|
|
73
|
-
projectInfo = (await getProjectInfo(this.
|
|
88
|
+
projectInfo = (await getProjectInfo(this.getToken(), baseUrl, projectKey));
|
|
74
89
|
}
|
|
75
90
|
catch (err) {
|
|
76
91
|
throw new Error(`Failed to auto-resolve viewId/folderPath/folderID for ${autoResolveConfig.moduleName} in project ${projectKey}. ` +
|
|
@@ -83,7 +98,7 @@ export class QmetryClient {
|
|
|
83
98
|
}
|
|
84
99
|
// Extract projectKey and baseUrl from arguments to prevent them from being sent in request body
|
|
85
100
|
const { projectKey: _, baseUrl: __, ...cleanArgs } = a;
|
|
86
|
-
const result = await handlerFn(this.
|
|
101
|
+
const result = await handlerFn(this.getToken(), baseUrl, projectKey, cleanArgs);
|
|
87
102
|
// Use custom formatter if available, otherwise return JSON
|
|
88
103
|
const formatted = tool.formatResponse
|
|
89
104
|
? tool.formatResponse(result)
|