@smartbear/mcp
Version:
MCP server for interacting SmartBear Products
378 lines (377 loc) • 15.6 kB
JavaScript
import { z } from "zod";
const ConfigurationSchema = z.object({
base_url: z.string().url().describe("Collaborator server base URL"),
username: z.string().describe("Collaborator username for authentication"),
login_ticket: z
.string()
.describe("Collaborator login ticket for authentication"),
});
export class CollaboratorClient {
name = "Collaborator";
toolPrefix = "collaborator";
configPrefix = "Collaborator";
config = ConfigurationSchema;
baseUrl;
username;
loginTicket;
async configure(_server, config, _cache) {
this.baseUrl = config.base_url;
this.username = config.username;
this.loginTicket = config.login_ticket;
return true;
}
/**
* Calls the Collaborator API with the given commands, prepending authentication automatically.
* @param commands Array of Collaborator API commands (excluding authentication)
* @returns Raw Collaborator API response
*/
async call(commands) {
if (!this.baseUrl || !this.username || !this.loginTicket) {
throw new Error("Collaborator client not configured");
}
const url = `${this.baseUrl}/services/json/v1`;
// Always prepend authentication command automatically
const body = [
{
command: "SessionService.authenticate",
args: { login: this.username, ticket: this.loginTicket },
},
...commands,
];
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`Collaborator API call failed: ${response.status} - ${await response.text()}`);
}
return await response.json();
}
/**
* Registers the Collaborator API tool with the MCP server. Accepts commands (excluding authentication).
*/
registerTools(register, _getInput) {
// findReviewById tool
register({
title: "Find Collaborator Review By ID",
summary: "Finds a review in Collaborator by its review ID.",
inputSchema: z.object({
reviewId: z.string().describe("The Collaborator review ID to find."),
}),
}, async (args, _extra) => {
const { reviewId } = args;
const commands = [
{
command: "ReviewService.findReviewById",
args: { reviewId },
},
];
const result = await this.call(commands);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
});
// createReview tool
register({
title: "Create Collaborator Review",
summary: "Creates a new review in Collaborator. All parameters are optional.",
inputSchema: z.object({
creator: z
.string()
.optional()
.describe("Collaborator username of the review creator. Optional. Default: currently logged in user."),
title: z
.string()
.optional()
.describe("Title of the review. Optional. Default: null."),
templateName: z
.string()
.optional()
.describe("Review template name. Optional. Default: system default template."),
accessPolicy: z
.string()
.optional()
.describe("Access policy for the review. Optional. Default: ANYONE."),
}),
}, async (args, _extra) => {
const commandArgs = {};
if (args.creator !== undefined)
commandArgs.creator = args.creator;
if (args.title !== undefined)
commandArgs.title = args.title;
if (args.templateName !== undefined)
commandArgs.templateName = args.templateName;
if (args.accessPolicy !== undefined)
commandArgs.accessPolicy = args.accessPolicy;
const commands = [
{
command: "ReviewService.createReview",
args: commandArgs,
},
];
const result = await this.call(commands);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
});
// rejectReview tool
register({
title: "Reject Collaborator Review",
summary: "Rejects a review in Collaborator by its review ID and reason.",
inputSchema: z.object({
reviewId: z
.union([z.string(), z.number()])
.describe("The Collaborator review ID to reject."),
reason: z.string().describe("Reason for rejecting the review."),
}),
}, async (args, _extra) => {
const { reviewId, reason } = args;
const commands = [
{
command: "ReviewService.reject",
args: {
reviewId,
reason,
},
},
];
const result = await this.call(commands);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
});
register({
title: "ReviewService Action",
summary: "Invoke any ReviewService method by name and arguments. For finishReviewPhase and waitOnPhase, provide reviewId (required) and until (optional, defaults to 'ANY').",
inputSchema: z.object({
action: z.enum([
"moveReviewToAnnotatePhase",
"cancel",
"reopen",
"uncancel",
]),
args: z.record(z.any()),
}),
}, async (params, _extra) => {
const { action, args } = params;
const commands = [{ command: `ReviewService.${action}`, args }];
const result = await this.call(commands);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
});
// getReviews tool
register({
title: "Get Collaborator Reviews",
summary: "Retrieves reviews from Collaborator using ReviewService.getReviews. All parameters are optional and only provided ones are sent.",
inputSchema: z.object({
login: z
.string()
.optional()
.describe("Collaborator username to filter reviews."),
role: z
.string()
.optional()
.describe("Role to filter reviews (e.g., AUTHOR)."),
creator: z
.boolean()
.optional()
.describe("Whether to filter by creator."),
reviewPhase: z
.string()
.optional()
.describe("Review phase to filter (e.g., PLANNING)."),
fullInfo: z
.boolean()
.optional()
.describe("Whether to retrieve full review info."),
fromDate: z
.string()
.optional()
.describe('Minimal creation date in format "yyyy-MM-dd"'),
toDate: z
.string()
.optional()
.describe('Maximal creation date in format "yyyy-MM-dd"'),
}),
}, async (args, _extra) => {
const reviewArgs = {};
if (args.login !== undefined)
reviewArgs.login = args.login;
if (args.role !== undefined)
reviewArgs.role = args.role;
if (args.creator !== undefined)
reviewArgs.creator = args.creator;
if (args.reviewPhase !== undefined)
reviewArgs.reviewPhase = args.reviewPhase;
if (args.fullInfo !== undefined)
reviewArgs.fullInfo = args.fullInfo;
if (args.fromDate !== undefined)
reviewArgs.fromDate = args.fromDate;
if (args.toDate !== undefined)
reviewArgs.toDate = args.toDate;
const commands = [
{
command: "ReviewService.getReviews",
args: reviewArgs,
},
];
const result = await this.call(commands);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
});
// createIntegration tool
register({
title: "Create Collaborator Remote System Configuration",
summary: "Creates a remote system configuration in Collaborator (e.g., Bitbucket, GitHub, etc).",
inputSchema: z.object({
token: z
.string()
.describe("Remote system token, e.g., BITBUCKET, GITHUB, etc."),
title: z.string().describe("Remote system title."),
config: z
.string()
.describe("JSON string containing configuration parameters for the remote system."),
reviewTemplateId: z
.string()
.optional()
.describe("Optional review template ID used by this remote system."),
}),
}, async (args, _extra) => {
const { token, title, config, reviewTemplateId } = args;
const commandArgs = { token, title, config };
if (reviewTemplateId)
commandArgs.reviewTemplateId = reviewTemplateId;
const commands = [
{
command: "AdminRemoteSystemService.createIntegration",
args: commandArgs,
},
];
const result = await this.call(commands);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
});
// editIntegration tool
register({
title: "Edit Collaborator Remote System Configuration",
summary: "Edits parameters of an existing remote system configuration in Collaborator. Only title and config are editable after creation.",
inputSchema: z.object({
id: z
.string()
.describe("ID of the remote system Configuration to edit."),
title: z.string().optional().describe("Remote system title."),
config: z
.string()
.optional()
.describe("JSON string containing configuration parameters for the remote system."),
reviewTemplateId: z
.string()
.optional()
.describe("Optional review template ID used by this remote system."),
}),
}, async (args, _extra) => {
const { id, title, config, reviewTemplateId } = args;
const commandArgs = { id };
if (title)
commandArgs.title = title;
if (config)
commandArgs.config = config;
if (reviewTemplateId)
commandArgs.reviewTemplateId = reviewTemplateId;
const commands = [
{
command: "AdminRemoteSystemService.editIntegration",
args: commandArgs,
},
];
const result = await this.call(commands);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
});
// deleteIntegration tool
register({
title: "Delete Collaborator Remote System Configuration",
summary: "Deletes a remote system configuration in Collaborator by its ID.",
inputSchema: z.object({
id: z
.union([z.string(), z.number()])
.describe("ID of the remote system Configuration to delete."),
}),
}, async (args, _extra) => {
const commandArgs = {};
if (args.id !== undefined)
commandArgs.id =
typeof args.id === "string" && !Number.isNaN(Number(args.id))
? Number(args.id)
: args.id;
const commands = [
{
command: "AdminRemoteSystemService.deleteIntegration",
args: commandArgs,
},
];
const result = await this.call(commands);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
});
register({
title: "Update Collaborator Remote System Configuration Webhook",
summary: "Updates the webhook for a remote system configuration in Collaborator by its ID.",
inputSchema: z.object({
id: z
.union([z.string(), z.number()])
.describe("ID of the remote system Configuration to update the webhook for."),
}),
}, async (args, _extra) => {
const commandArgs = {};
if (args.id !== undefined)
commandArgs.id =
typeof args.id === "string" && !Number.isNaN(Number(args.id))
? Number(args.id)
: args.id;
const commands = [
{
command: "AdminRemoteSystemService.updateWebhook",
args: commandArgs,
},
];
const result = await this.call(commands);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
});
// Test connection tool
register({
title: "Test Collaborator Remote System Configuration Connection",
summary: "Tests the connection for a remote system configuration in Collaborator by its ID.",
inputSchema: z.object({
id: z
.union([z.string(), z.number()])
.describe("ID of the remote system Configuration to test connection for."),
}),
}, async (args, _extra) => {
const commandArgs = {};
if (args.id !== undefined)
commandArgs.id =
typeof args.id === "string" && !Number.isNaN(Number(args.id))
? Number(args.id)
: args.id;
const commands = [
{
command: "AdminRemoteSystemService.testConnection",
args: commandArgs,
},
];
const result = await this.call(commands);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
});
}
}