UNPKG

@smartbear/mcp

Version:

MCP server for interacting SmartBear Products

121 lines (120 loc) 5.48 kB
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), }, ], }; })); } } }