@smartbear/mcp
Version:
MCP server for interacting SmartBear Products
121 lines (120 loc) • 5.48 kB
JavaScript
import z from "zod";
import { autoResolveViewIdAndFolderPath, findAutoResolveConfig, } from "./client/auto-resolve.js";
import { QMETRY_HANDLER_MAP } from "./client/handlers.js";
import { getProjectInfo } from "./client/project.js";
import { TOOLS } from "./client/tools/index.js";
import { QMETRY_DEFAULTS } from "./config/constants.js";
const ConfigurationSchema = z.object({
api_key: z.string().describe("QMetry API key for authentication"),
base_url: z
.string()
.url()
.optional()
.describe("Optional QMetry base URL for custom or region-specific endpoints"),
});
export class QmetryClient {
name = "QMetry";
toolPrefix = "qmetry";
configPrefix = "Qmetry";
config = ConfigurationSchema;
token;
projectApiKey = QMETRY_DEFAULTS.PROJECT_KEY;
endpoint = QMETRY_DEFAULTS.BASE_URL;
async configure(_server, config, _cache) {
this.token = config.api_key;
if (config.base_url) {
this.endpoint = config.base_url;
}
return true;
}
getToken() {
if (!this.token)
throw new Error("Client not configured");
return this.token;
}
getBaseUrl() {
return this.endpoint;
}
registerTools(register, _getInput) {
const resolveContext = (args) => ({
baseUrl: args.baseUrl ?? this.endpoint,
projectKey: args.projectKey ?? this.projectApiKey,
});
const handleAsync = async (fn) => {
try {
return await fn();
}
catch (err) {
return {
content: [
{
success: false,
type: "text",
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
},
],
};
}
};
for (const tool of TOOLS) {
const handlerFn = QMETRY_HANDLER_MAP[tool.handler];
if (!handlerFn) {
console.error(`⚠️ No handler mapped for ${tool.title}`);
continue;
}
register(tool, (args) => handleAsync(async () => {
const a = args;
const { baseUrl, projectKey } = resolveContext(a);
// Dynamic auto-resolve for modules that support viewId, folderPath, and folderID
const autoResolveConfig = findAutoResolveConfig(tool.handler);
if (autoResolveConfig) {
// Check if we need to auto-resolve viewId, folderPath, or folderID
const needsViewIdResolve = !a.viewId && autoResolveConfig.viewIdPath;
const needsFolderPathResolve = a.folderPath === undefined;
const needsFolderIdResolve = autoResolveConfig.folderIdField &&
!a[autoResolveConfig.folderIdField];
// Explicit condition for auto-resolving tcFolderID for Create Test Case
const needsTcFolderIdAutoResolve = autoResolveConfig.folderIdField === "tcFolderID" && !a.tcFolderID;
// Explicit condition for auto-resolving parentFolderId for Create Test Suite
const needsParentFolderIdAutoResolve = autoResolveConfig.folderIdField === "parentFolderId" &&
!a.parentFolderId;
if (needsViewIdResolve ||
needsFolderPathResolve ||
needsFolderIdResolve ||
needsTcFolderIdAutoResolve ||
needsParentFolderIdAutoResolve) {
let projectInfo;
try {
projectInfo = (await getProjectInfo(this.getToken(), baseUrl, projectKey));
}
catch (err) {
throw new Error(`Failed to auto-resolve viewId/folderPath/folderID for ${autoResolveConfig.moduleName} in project ${projectKey}. ` +
`Please provide them manually or check project access. ` +
`Error: ${err instanceof Error ? err.message : String(err)}`);
}
// Apply auto-resolution using the dynamic configuration
Object.assign(a, autoResolveViewIdAndFolderPath(a, projectInfo, autoResolveConfig));
}
}
// Extract projectKey and baseUrl from arguments to prevent them from being sent in request body
const { projectKey: _, baseUrl: __, ...cleanArgs } = a;
const result = await handlerFn(this.getToken(), baseUrl, projectKey, cleanArgs);
// Use custom formatter if available, otherwise return JSON
const formatted = tool.formatResponse
? tool.formatResponse(result)
: (result ?? {});
return {
content: [
{
success: true,
type: "text",
text: typeof formatted === "string"
? formatted
: JSON.stringify(formatted, null, 2),
},
],
};
}));
}
}
}