🚀 Big News: Socket Acquires Coana to Bring Reachability Analysis to Every Appsec Team.Learn more

bitbucket-mcp

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

bitbucket-mcp - npm Package Compare versions

Comparing version

to
1.1.0

#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, InitializeRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
import winston from "winston";
import process from "node:process";
// No console logs, only file logging
// Constants
const LOG_FILE = "bitbucket.log";
const API_VERSION = "1.1.0";
const API_PATH = "/rest/api/1.0";
// Enums
var MergeStrategy;
(function (MergeStrategy) {
MergeStrategy["MERGE_COMMIT"] = "merge-commit";
MergeStrategy["SQUASH"] = "squash";
MergeStrategy["FAST_FORWARD"] = "fast-forward";
})(MergeStrategy || (MergeStrategy = {}));
var ReviewAction;
(function (ReviewAction) {
ReviewAction["APPROVED"] = "APPROVED";
ReviewAction["REVIEWED"] = "REVIEWED";
})(ReviewAction || (ReviewAction = {}));
// Configuration for logger
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || "info",
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [
new winston.transports.File({ filename: "bitbucket.log" }),
...(process.env.DEBUG === "true" ? [new winston.transports.Console()] : []),
],
level: "info",
format: winston.format.json(),
transports: [new winston.transports.File({ filename: LOG_FILE })],
});

@@ -20,8 +32,7 @@ class BitbucketServer {

api;
cloudApi;
config;
constructor() {
this.server = new Server({
name: "bitbucket-mcp",
version: "1.0.2",
name: "bitbucket-server-mcp-server",
version: API_VERSION,
}, {

@@ -32,47 +43,43 @@ capabilities: {

});
// Configuration initiale à partir des variables d'environnement
this.config = {
// Configuration from environment variables
this.config = this.loadConfig();
// Configuration of Axios instance
this.api = axios.create({
baseURL: `${this.config.baseUrl}${API_PATH}`,
headers: this.config.token
? { Authorization: `Bearer ${this.config.token}` }
: {},
auth: this.config.username && this.config.password
? { username: this.config.username, password: this.config.password }
: undefined,
});
this.setupToolHandlers();
this.server.onerror = (error) => {
logger.error("[MCP Error]", error);
};
}
/**
* Loads configuration from environment variables
* @returns BitbucketConfig configuration object
* @throws Error if required environment variables are not set
*/
loadConfig() {
const config = {
baseUrl: process.env.BITBUCKET_URL ?? "",
token: process.env.BITBUCKET_TOKEN,
username: process.env.BITBUCKET_USERNAME,
appPassword: process.env.BITBUCKET_APP_PASSWORD,
password: process.env.BITBUCKET_PASSWORD,
defaultProject: process.env.BITBUCKET_DEFAULT_PROJECT,
cloudBaseUrl: process.env.BITBUCKET_CLOUD_URL ?? "https://api.bitbucket.org/2.0",
};
if (!this.config.baseUrl) {
if (!config.baseUrl) {
throw new Error("BITBUCKET_URL is required");
}
if (!this.config.token &&
!(this.config.username && this.config.appPassword)) {
throw new Error("Either BITBUCKET_TOKEN or BITBUCKET_USERNAME/BITBUCKET_APP_PASSWORD is required");
if (!config.token && !(config.username && config.password)) {
throw new Error("Either BITBUCKET_TOKEN or BITBUCKET_USERNAME/PASSWORD is required");
}
// Configuration de l'instance Axios
this.api = axios.create({
baseURL: `${this.config.baseUrl}/rest/api/1.0`,
headers: this.config.token
? { Authorization: `Bearer ${this.config.token}` }
: {},
auth: this.config.username && this.config.appPassword
? {
username: this.config.username,
password: this.config.appPassword,
}
: undefined,
});
// Configuration de l'instance Axios pour Bitbucket Cloud
this.cloudApi = axios.create({
baseURL: this.config.cloudBaseUrl,
headers: this.config.token
? { Authorization: `Bearer ${this.config.token}` }
: {},
auth: this.config.username && this.config.appPassword
? {
username: this.config.username,
password: this.config.appPassword,
}
: undefined,
});
this.setupToolHandlers();
this.server.onerror = (error) => logger.error("[MCP Error]", error);
return config;
}
/**
* Type guard for PullRequestInput
*/
isPullRequestInput(args) {

@@ -91,11 +98,6 @@ const input = args;

}
/**
* Sets up tool handlers for the MCP server
*/
setupToolHandlers() {
// Add initialization handler
this.server.setRequestHandler(InitializeRequestSchema, async () => ({
name: "bitbucket-mcp",
version: "1.0.2",
capabilities: {
tools: {},
},
}));
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({

@@ -155,3 +157,3 @@ tools: [

type: "string",
enum: ["merge-commit", "squash", "fast-forward"],
enum: Object.values(MergeStrategy),
description: "Merge strategy to use",

@@ -225,213 +227,2 @@ },

},
{
name: "list_public_repositories",
description: "List public repositories in Bitbucket Cloud",
inputSchema: {
type: "object",
properties: {
role: {
type: "string",
description: "Filter by role: admin, contributor, member, owner",
},
q: {
type: "string",
description: "Query string to filter repositories",
},
sort: { type: "string", description: "Field to sort results by" },
},
},
},
{
name: "list_workspace_repositories",
description: "List repositories in a workspace in Bitbucket Cloud",
inputSchema: {
type: "object",
properties: {
workspace: {
type: "string",
description: "Workspace identifier",
},
role: {
type: "string",
description: "Filter by role: admin, contributor, member, owner",
},
q: {
type: "string",
description: "Query string to filter repositories",
},
sort: { type: "string", description: "Field to sort results by" },
},
required: ["workspace"],
},
},
{
name: "get_repository",
description: "Get a repository in Bitbucket Cloud",
inputSchema: {
type: "object",
properties: {
workspace: {
type: "string",
description: "Workspace identifier",
},
repo_slug: { type: "string", description: "Repository slug" },
},
required: ["workspace", "repo_slug"],
},
},
{
name: "list_repository_forks",
description: "List forks of a repository in Bitbucket Cloud",
inputSchema: {
type: "object",
properties: {
workspace: {
type: "string",
description: "Workspace identifier",
},
repo_slug: { type: "string", description: "Repository slug" },
},
required: ["workspace", "repo_slug"],
},
},
{
name: "list_repository_webhooks",
description: "List webhooks for a repository in Bitbucket Cloud",
inputSchema: {
type: "object",
properties: {
workspace: {
type: "string",
description: "Workspace identifier",
},
repo_slug: { type: "string", description: "Repository slug" },
},
required: ["workspace", "repo_slug"],
},
},
{
name: "get_repository_webhook",
description: "Get a webhook for a repository in Bitbucket Cloud",
inputSchema: {
type: "object",
properties: {
workspace: {
type: "string",
description: "Workspace identifier",
},
repo_slug: { type: "string", description: "Repository slug" },
uid: { type: "string", description: "Webhook identifier" },
},
required: ["workspace", "repo_slug", "uid"],
},
},
{
name: "get_repository_settings_inheritance",
description: "Retrieve the inheritance state for repository settings in Bitbucket Cloud",
inputSchema: {
type: "object",
properties: {
workspace: {
type: "string",
description: "Workspace identifier",
},
repo_slug: { type: "string", description: "Repository slug" },
},
required: ["workspace", "repo_slug"],
},
},
{
name: "list_repository_group_permissions",
description: "List explicit group permissions for a repository in Bitbucket Cloud",
inputSchema: {
type: "object",
properties: {
workspace: {
type: "string",
description: "Workspace identifier",
},
repo_slug: { type: "string", description: "Repository slug" },
},
required: ["workspace", "repo_slug"],
},
},
{
name: "get_repository_group_permission",
description: "Get an explicit group permission for a repository in Bitbucket Cloud",
inputSchema: {
type: "object",
properties: {
workspace: {
type: "string",
description: "Workspace identifier",
},
repo_slug: { type: "string", description: "Repository slug" },
group_slug: { type: "string", description: "Group slug" },
},
required: ["workspace", "repo_slug", "group_slug"],
},
},
{
name: "list_repository_user_permissions",
description: "List explicit user permissions for a repository in Bitbucket Cloud",
inputSchema: {
type: "object",
properties: {
workspace: {
type: "string",
description: "Workspace identifier",
},
repo_slug: { type: "string", description: "Repository slug" },
},
required: ["workspace", "repo_slug"],
},
},
{
name: "get_repository_user_permission",
description: "Get an explicit user permission for a repository in Bitbucket Cloud",
inputSchema: {
type: "object",
properties: {
workspace: {
type: "string",
description: "Workspace identifier",
},
repo_slug: { type: "string", description: "Repository slug" },
selected_user_id: {
type: "string",
description: "Selected user identifier",
},
},
required: ["workspace", "repo_slug", "selected_user_id"],
},
},
{
name: "list_repository_watchers",
description: "List watchers of a repository in Bitbucket Cloud",
inputSchema: {
type: "object",
properties: {
workspace: {
type: "string",
description: "Workspace identifier",
},
repo_slug: { type: "string", description: "Repository slug" },
},
required: ["workspace", "repo_slug"],
},
},
{
name: "list_user_repository_permissions",
description: "List repository permissions for a user in Bitbucket Cloud",
inputSchema: {
type: "object",
properties: {
q: {
type: "string",
description: "Query string to filter permissions",
},
sort: { type: "string", description: "Field to sort results by" },
},
},
},
],

@@ -445,35 +236,3 @@ }));

const args = request.params.arguments ?? {};
const pullRequestParams = {
project: args.project ?? this.config.defaultProject,
repository: args.repository,
prId: args.prId,
};
const workspaceRepoParams = {
workspace: args.workspace,
repo_slug: args.repo_slug,
};
const webhookParams = {
...workspaceRepoParams,
uid: args.uid,
};
const groupPermissionParams = {
...workspaceRepoParams,
group_slug: args.group_slug,
};
const userPermissionParams = {
...workspaceRepoParams,
selected_user_id: args.selected_user_id,
};
if (!pullRequestParams.project &&
[
"create_pull_request",
"get_pull_request",
"merge_pull_request",
"decline_pull_request",
"add_comment",
"get_diff",
"get_reviews",
].includes(request.params.name)) {
throw new McpError(ErrorCode.InvalidParams, "Project must be provided either as a parameter or through BITBUCKET_DEFAULT_PROJECT environment variable");
}
const pullRequestParams = this.extractPullRequestParams(args);
switch (request.params.name) {

@@ -503,85 +262,2 @@ case "create_pull_request":

return await this.getReviews(pullRequestParams);
case "list_public_repositories":
return await this.listPublicRepositories({
role: args.role,
q: args.q,
sort: args.sort,
});
case "list_workspace_repositories":
if (!workspaceRepoParams.workspace) {
throw new McpError(ErrorCode.InvalidParams, "Workspace must be provided");
}
return await this.listWorkspaceRepositories(workspaceRepoParams.workspace, {
role: args.role,
q: args.q,
sort: args.sort,
});
case "get_repository":
if (!workspaceRepoParams.workspace ||
!workspaceRepoParams.repo_slug) {
throw new McpError(ErrorCode.InvalidParams, "Workspace and repo_slug must be provided");
}
return await this.getRepository(workspaceRepoParams);
case "list_repository_forks":
if (!workspaceRepoParams.workspace ||
!workspaceRepoParams.repo_slug) {
throw new McpError(ErrorCode.InvalidParams, "Workspace and repo_slug must be provided");
}
return await this.listRepositoryForks(workspaceRepoParams);
case "list_repository_webhooks":
if (!workspaceRepoParams.workspace ||
!workspaceRepoParams.repo_slug) {
throw new McpError(ErrorCode.InvalidParams, "Workspace and repo_slug must be provided");
}
return await this.listRepositoryWebhooks(workspaceRepoParams);
case "get_repository_webhook":
if (!webhookParams.workspace ||
!webhookParams.repo_slug ||
!webhookParams.uid) {
throw new McpError(ErrorCode.InvalidParams, "Workspace, repo_slug, and uid must be provided");
}
return await this.getRepositoryWebhook(webhookParams);
case "get_repository_settings_inheritance":
if (!workspaceRepoParams.workspace ||
!workspaceRepoParams.repo_slug) {
throw new McpError(ErrorCode.InvalidParams, "Workspace and repo_slug must be provided");
}
return await this.getRepositorySettingsInheritance(workspaceRepoParams);
case "list_repository_group_permissions":
if (!workspaceRepoParams.workspace ||
!workspaceRepoParams.repo_slug) {
throw new McpError(ErrorCode.InvalidParams, "Workspace and repo_slug must be provided");
}
return await this.listRepositoryGroupPermissions(workspaceRepoParams);
case "get_repository_group_permission":
if (!groupPermissionParams.workspace ||
!groupPermissionParams.repo_slug ||
!groupPermissionParams.group_slug) {
throw new McpError(ErrorCode.InvalidParams, "Workspace, repo_slug, and group_slug must be provided");
}
return await this.getRepositoryGroupPermission(groupPermissionParams);
case "list_repository_user_permissions":
if (!workspaceRepoParams.workspace ||
!workspaceRepoParams.repo_slug) {
throw new McpError(ErrorCode.InvalidParams, "Workspace and repo_slug must be provided");
}
return await this.listRepositoryUserPermissions(workspaceRepoParams);
case "get_repository_user_permission":
if (!userPermissionParams.workspace ||
!userPermissionParams.repo_slug ||
!userPermissionParams.selected_user_id) {
throw new McpError(ErrorCode.InvalidParams, "Workspace, repo_slug, and selected_user_id must be provided");
}
return await this.getRepositoryUserPermission(userPermissionParams);
case "list_repository_watchers":
if (!workspaceRepoParams.workspace ||
!workspaceRepoParams.repo_slug) {
throw new McpError(ErrorCode.InvalidParams, "Workspace and repo_slug must be provided");
}
return await this.listRepositoryWatchers(workspaceRepoParams);
case "list_user_repository_permissions":
return await this.listUserRepositoryPermissions({
q: args.q,
sort: args.sort,
});
default:

@@ -593,11 +269,40 @@ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);

logger.error("Tool execution error", { error });
if (axios.isAxiosError(error)) {
throw new McpError(ErrorCode.InternalError, `Bitbucket API error: ${error.response?.data.message ?? error.message}`);
}
throw error;
return this.handleError(error);
}
});
}
/**
* Extracts pull request parameters from the arguments
*/
extractPullRequestParams(args) {
const pullRequestParams = {
project: args.project ?? this.config.defaultProject ?? "",
repository: args.repository,
prId: args.prId,
};
if (!pullRequestParams.project) {
throw new McpError(ErrorCode.InvalidParams, "Project must be provided either as a parameter or through BITBUCKET_DEFAULT_PROJECT environment variable");
}
return pullRequestParams;
}
/**
* Handles errors and converts them to McpError format
*/
handleError(error) {
if (error instanceof McpError) {
throw error;
}
if (axios.isAxiosError(error)) {
const axiosError = error;
const statusCode = axiosError.response?.status ?? 500;
const errorMessage = axiosError.response?.data?.message ?? axiosError.message;
throw new McpError(ErrorCode.InternalError, `Bitbucket API error (${statusCode}): ${errorMessage}`);
}
throw new McpError(ErrorCode.InternalError, `Unknown error: ${error instanceof Error ? error.message : String(error)}`);
}
/**
* Creates a new pull request
*/
async createPullRequest(input) {
const response = await this.api.post(`/projects/${input.project}/repos/${input.repository}/pull-requests`, {
const payload = {
title: input.title,

@@ -622,3 +327,4 @@ description: input.description,

})),
});
};
const response = await this.api.post(`/projects/${input.project}/repos/${input.repository}/pull-requests`, payload);
return {

@@ -628,2 +334,5 @@ content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],

}
/**
* Gets pull request details
*/
async getPullRequest(params) {

@@ -636,5 +345,8 @@ const { project, repository, prId } = params;

}
/**
* Merges a pull request
*/
async mergePullRequest(params, options = {}) {
const { project, repository, prId } = params;
const { message, strategy = "merge-commit" } = options;
const { message, strategy = MergeStrategy.MERGE_COMMIT } = options;
const response = await this.api.post(`/projects/${project}/repos/${repository}/pull-requests/${prId}/merge`, {

@@ -649,2 +361,5 @@ version: -1,

}
/**
* Declines a pull request
*/
async declinePullRequest(params, message) {

@@ -660,2 +375,5 @@ const { project, repository, prId } = params;

}
/**
* Adds a comment to a pull request
*/
async addComment(params, options) {

@@ -672,2 +390,5 @@ const { project, repository, prId } = params;

}
/**
* Gets the diff for a pull request
*/
async getDiff(params, contextLines = 10) {

@@ -683,6 +404,10 @@ const { project, repository, prId } = params;

}
/**
* Gets the reviews for a pull request
*/
async getReviews(params) {
const { project, repository, prId } = params;
const response = await this.api.get(`/projects/${project}/repos/${repository}/pull-requests/${prId}/activities`);
const reviews = response.data.values.filter((activity) => activity.action === "APPROVED" || activity.action === "REVIEWED");
const reviews = response.data.values.filter((activity) => activity.action === ReviewAction.APPROVED ||
activity.action === ReviewAction.REVIEWED);
return {

@@ -692,128 +417,11 @@ content: [{ type: "text", text: JSON.stringify(reviews, null, 2) }],

}
// Bitbucket Cloud Repository API endpoints implementation
async listPublicRepositories(options) {
const response = await this.cloudApi.get("/repositories", {
params: options,
});
return {
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
};
}
async listWorkspaceRepositories(workspace, options) {
const response = await this.cloudApi.get(`/repositories/${workspace}`, {
params: options,
});
return {
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
};
}
async getRepository(params) {
const { workspace, repo_slug } = params;
const response = await this.cloudApi.get(`/repositories/${workspace}/${repo_slug}`);
return {
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
};
}
async listRepositoryForks(params) {
const { workspace, repo_slug } = params;
const response = await this.cloudApi.get(`/repositories/${workspace}/${repo_slug}/forks`);
return {
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
};
}
async listRepositoryWebhooks(params) {
const { workspace, repo_slug } = params;
const response = await this.cloudApi.get(`/repositories/${workspace}/${repo_slug}/hooks`);
return {
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
};
}
async getRepositoryWebhook(params) {
const { workspace, repo_slug, uid } = params;
const response = await this.cloudApi.get(`/repositories/${workspace}/${repo_slug}/hooks/${uid}`);
return {
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
};
}
async getRepositorySettingsInheritance(params) {
const { workspace, repo_slug } = params;
const response = await this.cloudApi.get(`/repositories/${workspace}/${repo_slug}/override-settings`);
return {
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
};
}
async listRepositoryGroupPermissions(params) {
const { workspace, repo_slug } = params;
const response = await this.cloudApi.get(`/repositories/${workspace}/${repo_slug}/permissions-config/groups`);
return {
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
};
}
async getRepositoryGroupPermission(params) {
const { workspace, repo_slug, group_slug } = params;
const response = await this.cloudApi.get(`/repositories/${workspace}/${repo_slug}/permissions-config/groups/${group_slug}`);
return {
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
};
}
async listRepositoryUserPermissions(params) {
const { workspace, repo_slug } = params;
const response = await this.cloudApi.get(`/repositories/${workspace}/${repo_slug}/permissions-config/users`);
return {
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
};
}
async getRepositoryUserPermission(params) {
const { workspace, repo_slug, selected_user_id } = params;
const response = await this.cloudApi.get(`/repositories/${workspace}/${repo_slug}/permissions-config/users/${selected_user_id}`);
return {
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
};
}
async listRepositoryWatchers(params) {
const { workspace, repo_slug } = params;
const response = await this.cloudApi.get(`/repositories/${workspace}/${repo_slug}/watchers`);
return {
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
};
}
async listUserRepositoryPermissions(options) {
const response = await this.cloudApi.get("/user/permissions/repositories", {
params: options,
});
return {
content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }],
};
}
/**
* Starts the server
*/
async run() {
try {
const transport = new StdioServerTransport();
await this.server.connect(transport);
logger.info("Bitbucket MCP server running on stdio");
logger.info(`Version: 1.0.2`);
logger.info(`Using Cloud API: ${this.config.cloudBaseUrl}`);
if (this.config.baseUrl) {
logger.info(`Using Server API: ${this.config.baseUrl}`);
}
// Log authentication method used (without revealing secrets)
if (this.config.token) {
logger.info("Authentication: Using token");
}
else if (this.config.username && this.config.appPassword) {
logger.info(`Authentication: Using basic auth with username ${this.config.username}`);
}
}
catch (error) {
logger.error("Failed to start MCP server", { error });
process.exit(1);
}
const transport = new StdioServerTransport();
await this.server.connect(transport);
logger.info("Bitbucket MCP server running on stdio");
}
}
// No console.log statements in MCP mode
// Handle uncaught exceptions
process.on("uncaughtException", (error) => {
logger.error("Uncaught exception", { error });
process.exit(1);
});
// Run the server
const server = new BitbucketServer();

@@ -820,0 +428,0 @@ server.run().catch((error) => {

{
"name": "bitbucket-mcp",
"version": "1.0.5",
"version": "1.1.0",
"description": "Model Context Protocol (MCP) server for Bitbucket Cloud and Server API integration",

@@ -5,0 +5,0 @@ "type": "module",

Sorry, the diff of this file is not supported yet