Skip to main content
Glama
bitbucket.rs82.8 kB
use serde::{Serialize, Deserialize}; #[derive(Debug, Serialize, Deserialize)] pub struct BitbucketCommentContent { pub raw: String, } /// Represents inline comment positioning in Bitbucket pull requests. /// /// Used to attach comments to specific lines or line ranges in code files. /// /// # Fields /// * `from` - Starting line number (1-based). None for new file comments. /// * `to` - Ending line number (1-based). None for single-line comments. /// * `path` - Relative path to the file in the repository. #[derive(Debug, Serialize, Deserialize)] pub struct BitbucketInline { #[serde(skip_serializing_if = "Option::is_none")] pub from: Option<i32>, #[serde(skip_serializing_if = "Option::is_none")] pub to: Option<i32>, pub path: String, } #[derive(Debug, Serialize, Deserialize)] pub struct BitbucketCommentPayload { pub content: BitbucketCommentContent, #[serde(skip_serializing_if = "Option::is_none")] pub inline: Option<BitbucketInline>, } /// Normalizes various Bitbucket comment input formats into a `BitbucketCommentPayload`. /// /// # Supported Input Formats /// - **Object with `content.raw` field**: `{ "content": { "raw": "comment text" }, ... }` /// - **Object with `body` field**: `{ "body": "comment text", ... }` /// - **Direct string**: `"comment text"` /// /// The function checks for these formats in the following precedence order: /// 1. `content.raw` (highest priority) /// 2. `body` /// 3. Direct string value /// /// # Inline Comment Parameters /// If the input contains an `inline` object with a `path` field, optional `from` and `to` fields /// are also extracted. Example: /// ```json /// { "content": { "raw": "comment" }, "inline": { "path": "file.rs", "from": 10, "to": 12 } } /// ``` /// /// # Errors /// Returns an error if no valid comment string is found in any supported format. pub fn normalize_comment_input(body: serde_json::Value) -> Result<BitbucketCommentPayload, String> { let mut comment_raw: Option<String> = None; let mut inline_data: Option<BitbucketInline> = None; // Extract comment content if let Some(content) = body.get("content") { if let Some(raw) = content.get("raw") { comment_raw = Some(raw.as_str().unwrap_or("").to_string()); } } else if let Some(raw) = body.get("body") { comment_raw = Some(raw.as_str().unwrap_or("").to_string()); } else if let Some(s) = body.as_str() { comment_raw = Some(s.to_string()); } // Extract inline data if present if let Some(inline) = body.get("inline") { if let Some(path) = inline.get("path").and_then(|v| v.as_str()) { // Safely convert i64 to i32 with range validation let from = match inline.get("from").and_then(|v| v.as_i64()) { Some(v) => Some(i32::try_from(v).map_err(|_| format!("'from' line number {} out of valid range", v))?), None => None, }; let to = match inline.get("to").and_then(|v| v.as_i64()) { Some(v) => Some(i32::try_from(v).map_err(|_| format!("'to' line number {} out of valid range", v))?), None => None, }; inline_data = Some(BitbucketInline { from, to, path: path.to_string(), }); } } if let Some(raw) = comment_raw { return Ok(BitbucketCommentPayload { content: BitbucketCommentContent { raw }, inline: inline_data, }); } Err("Invalid comment input format".to_string()) } // Bitbucket MCP Tool Implementation // This module provides MCP tools for Bitbucket Cloud REST API integration. // Credentials are fetched from environment variables: BITBUCKET_USERNAME, BITBUCKET_APP_PASSWORD use std::env; use anyhow::{Result, anyhow}; use reqwest::{Client}; use rmcp::{Error as McpError, ServerHandler, model::*, schemars, tool}; #[derive(Clone)] pub struct BitbucketClient { pub api_username: String, pub app_password: String, pub client: Client, pub base_url: String, } impl BitbucketClient { // --- Pull Requests --- /// Create a bitbucket pull request pub async fn create_pullrequest(&self, workspace: &str, repo_slug: &str, body: serde_json::Value) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/pullrequests", self.base_url, workspace, repo_slug); let req = self.client.post(&url).json(&body); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } /// Get bitbucket pull request details pub async fn get_pullrequest(&self, workspace: &str, repo_slug: &str, pr_id: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/pullrequests/{}", self.base_url, workspace, repo_slug, pr_id); let req = self.client.get(&url); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } /// Update a bitbucket pull request pub async fn update_pullrequest(&self, workspace: &str, repo_slug: &str, pr_id: &str, body: serde_json::Value) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/pullrequests/{}", self.base_url, workspace, repo_slug, pr_id); let req = self.client.put(&url).json(&body); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } /// Approve a bitbucket pull request pub async fn approve_pullrequest(&self, workspace: &str, repo_slug: &str, pr_id: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/pullrequests/{}/approve", self.base_url, workspace, repo_slug, pr_id); let req = self.client.post(&url); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } /// Unapprove a bitbucket pull request pub async fn unapprove_pullrequest(&self, workspace: &str, repo_slug: &str, pr_id: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/pullrequests/{}/approve", self.base_url, workspace, repo_slug, pr_id); let req = self.client.delete(&url); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } /// Decline a bitbucket pull request pub async fn decline_pullrequest(&self, workspace: &str, repo_slug: &str, pr_id: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/pullrequests/{}/decline", self.base_url, workspace, repo_slug, pr_id); let req = self.client.post(&url); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } /// Merge a bitbucket pull request pub async fn merge_pullrequest(&self, workspace: &str, repo_slug: &str, pr_id: &str, body: Option<serde_json::Value>) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/pullrequests/{}/merge", self.base_url, workspace, repo_slug, pr_id); let req = if let Some(b) = body { self.client.post(&url).json(&b) } else { self.client.post(&url) }; let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } /// List bitbucket pull request comments with pagination support pub async fn list_pullrequest_comments(&self, workspace: &str, repo_slug: &str, pr_id: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/pullrequests/{}/comments", self.base_url, workspace, repo_slug, pr_id); self.fetch_paginated(url).await } /// Add a bitbucket pull request comment pub async fn add_pullrequest_comment(&self, workspace: &str, repo_slug: &str, pr_id: &str, body: BitbucketCommentPayload) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/pullrequests/{}/comments", self.base_url, workspace, repo_slug, pr_id); let req = self.client.post(&url).json(&body); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } /// List bitbucket pull request activity pub async fn list_pullrequest_activity(&self, workspace: &str, repo_slug: &str, pr_id: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/pullrequests/{}/activity", self.base_url, workspace, repo_slug, pr_id); self.fetch_paginated(url).await } /// Get bitbucket pull request diff pub async fn get_pullrequest_diff(&self, workspace: &str, repo_slug: &str, pr_id: &str) -> Result<String> { let url = format!("{}/repositories/{}/{}/pullrequests/{}/diff", self.base_url, workspace, repo_slug, pr_id); let req = self.client.get(&url); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.text().await?) } /// Get bitbucket pull request commits with pagination pub async fn list_pullrequest_commits(&self, workspace: &str, repo_slug: &str, pr_id: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/pullrequests/{}/commits", self.base_url, workspace, repo_slug, pr_id); self.fetch_paginated(url).await } /// List bitbucket pull request tasks with pagination pub async fn list_pullrequest_tasks(&self, workspace: &str, repo_slug: &str, pr_id: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/pullrequests/{}/tasks", self.base_url, workspace, repo_slug, pr_id); self.fetch_paginated(url).await } /// Add a bitbucket pull request task pub async fn add_pullrequest_task(&self, workspace: &str, repo_slug: &str, pr_id: &str, body: serde_json::Value) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/pullrequests/{}/tasks", self.base_url, workspace, repo_slug, pr_id); let req = self.client.post(&url).json(&body); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } /// Get bitbucket pull request diffstat pub async fn get_pullrequest_diffstat(&self, workspace: &str, repo_slug: &str, pr_id: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/pullrequests/{}/diffstat", self.base_url, workspace, repo_slug, pr_id); let req = self.client.get(&url); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } pub fn from_env() -> Result<Self> { let api_username = env::var("BITBUCKET_API_USERNAME") .map_err(|_| anyhow!("BITBUCKET_API_USERNAME env var not set. Please set it to your Atlassian email."))?; let app_password = env::var("BITBUCKET_APP_PASSWORD") .map_err(|_| anyhow!("BITBUCKET_APP_PASSWORD env var not set. Please set it to your Bitbucket API token."))?; Ok(Self { api_username, app_password, client: Client::new(), base_url: "https://api.bitbucket.org/2.0".to_string(), }) } fn apply_auth(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder { req.basic_auth(&self.api_username, Some(&self.app_password)) } /// Helper method to handle paginated API responses /// Fetches all pages and aggregates results into a single response /// /// # Safety /// Includes a maximum page limit of 1000 to prevent infinite loops /// in case of malformed API responses or circular pagination links. async fn fetch_paginated(&self, initial_url: String) -> Result<serde_json::Value> { const MAX_PAGES: usize = 1000; let mut all_values = Vec::new(); let mut url = initial_url; let mut page_count = 0; loop { page_count += 1; if page_count > MAX_PAGES { return Err(anyhow!("Exceeded maximum page limit ({}) - possible circular pagination", MAX_PAGES)); } let req = self.client.get(&url); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } let page: serde_json::Value = resp.json().await?; // Collect values from this page if let Some(values) = page.get("values").and_then(|v| v.as_array()) { all_values.extend_from_slice(values); } // Check if there's a next page if let Some(next_url) = page.get("next").and_then(|v| v.as_str()) { url = next_url.to_string(); } else { // No more pages, return combined results return Ok(serde_json::json!({ "values": all_values, "size": all_values.len() })); } } } pub async fn get_user(&self) -> Result<serde_json::Value> { let url = format!("{}/user", self.base_url); let req = self.client.get(&url); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } pub async fn list_workspaces(&self) -> Result<serde_json::Value> { let url = format!("{}/workspaces", self.base_url); self.fetch_paginated(url).await } pub async fn list_repositories(&self, workspace: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}", self.base_url, workspace); self.fetch_paginated(url).await } pub async fn list_pullrequests(&self, workspace: &str, repo_slug: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/pullrequests", self.base_url, workspace, repo_slug); self.fetch_paginated(url).await } pub async fn list_issues(&self, workspace: &str, repo_slug: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/issues", self.base_url, workspace, repo_slug); self.fetch_paginated(url).await } pub async fn get_workspace(&self, workspace: &str) -> Result<serde_json::Value> { let url = format!("{}/workspaces/{}", self.base_url, workspace); let req = self.client.get(&url); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } pub async fn get_repository(&self, workspace: &str, repo_slug: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}", self.base_url, workspace, repo_slug); let req = self.client.get(&url); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } pub async fn list_branches(&self, workspace: &str, repo_slug: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/refs/branches", self.base_url, workspace, repo_slug); self.fetch_paginated(url).await } pub async fn list_tags(&self, workspace: &str, repo_slug: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/refs/tags", self.base_url, workspace, repo_slug); self.fetch_paginated(url).await } pub async fn list_commits(&self, workspace: &str, repo_slug: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/commits", self.base_url, workspace, repo_slug); self.fetch_paginated(url).await } pub async fn list_pipelines(&self, workspace: &str, repo_slug: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/pipelines/", self.base_url, workspace, repo_slug); self.fetch_paginated(url).await } pub async fn list_deployments(&self, workspace: &str, repo_slug: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/deployments/", self.base_url, workspace, repo_slug); self.fetch_paginated(url).await } pub async fn list_downloads(&self, workspace: &str, repo_slug: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/downloads", self.base_url, workspace, repo_slug); self.fetch_paginated(url).await } pub async fn list_webhooks(&self, workspace: &str, repo_slug: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/hooks", self.base_url, workspace, repo_slug); self.fetch_paginated(url).await } pub async fn list_snippets(&self, workspace: &str) -> Result<serde_json::Value> { let url = format!("{}/snippets/{}", self.base_url, workspace); self.fetch_paginated(url).await } pub async fn list_projects(&self, workspace: &str) -> Result<serde_json::Value> { let url = format!("{}/workspaces/{}/projects", self.base_url, workspace); self.fetch_paginated(url).await } pub async fn list_branch_restrictions(&self, workspace: &str, repo_slug: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/branch-restrictions", self.base_url, workspace, repo_slug); self.fetch_paginated(url).await } pub async fn list_commit_statuses(&self, workspace: &str, repo_slug: &str, commit: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/commit/{}/statuses", self.base_url, workspace, repo_slug, commit); self.fetch_paginated(url).await } pub async fn list_users(&self, workspace: &str) -> Result<serde_json::Value> { let url = format!("{}/workspaces/{}/members", self.base_url, workspace); self.fetch_paginated(url).await } /// Create a repository in a workspace pub async fn create_repository(&self, workspace: &str, repo_slug: &str, body: serde_json::Value) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}", self.base_url, workspace, repo_slug); let req = self.client.post(&url).json(&body); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } /// Update a repository in a workspace pub async fn update_repository(&self, workspace: &str, repo_slug: &str, body: serde_json::Value) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}", self.base_url, workspace, repo_slug); let req = self.client.put(&url).json(&body); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } /// Delete a repository in a workspace pub async fn delete_repository(&self, workspace: &str, repo_slug: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}", self.base_url, workspace, repo_slug); let req = self.client.delete(&url); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } if resp.status() == reqwest::StatusCode::NO_CONTENT { return Ok(serde_json::json!({})); } Ok(resp.json().await?) } // --- Branches --- /// Create a branch in a repository pub async fn create_branch(&self, workspace: &str, repo_slug: &str, body: serde_json::Value) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/refs/branches", self.base_url, workspace, repo_slug); let req = self.client.post(&url).json(&body); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } /// Delete a branch in a repository pub async fn delete_branch(&self, workspace: &str, repo_slug: &str, branch: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/refs/branches/{}", self.base_url, workspace, repo_slug, branch); let req = self.client.delete(&url); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } if resp.status() == reqwest::StatusCode::NO_CONTENT { return Ok(serde_json::json!({})); } Ok(resp.json().await?) } // --- Branching Model --- pub async fn get_branching_model(&self, workspace: &str, repo_slug: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/branching-model", self.base_url, workspace, repo_slug); let req = self.client.get(&url); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } pub async fn update_branching_model(&self, workspace: &str, repo_slug: &str, body: serde_json::Value) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/branching-model", self.base_url, workspace, repo_slug); let req = self.client.put(&url).json(&body); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } // --- Commit Statuses --- pub async fn create_commit_status(&self, workspace: &str, repo_slug: &str, commit: &str, body: serde_json::Value) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/commit/{}/statuses/build", self.base_url, workspace, repo_slug, commit); let req = self.client.post(&url).json(&body); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } // --- Commits --- pub async fn get_commit(&self, workspace: &str, repo_slug: &str, commit: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/commit/{}", self.base_url, workspace, repo_slug, commit); let req = self.client.get(&url); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } // --- Deployments --- pub async fn create_deployment(&self, workspace: &str, repo_slug: &str, body: serde_json::Value) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/deployments/", self.base_url, workspace, repo_slug); let req = self.client.post(&url).json(&body); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } // --- Issue Tracker --- pub async fn create_issue(&self, workspace: &str, repo_slug: &str, body: serde_json::Value) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/issues", self.base_url, workspace, repo_slug); let req = self.client.post(&url).json(&body); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } pub async fn update_issue(&self, workspace: &str, repo_slug: &str, issue_id: &str, body: serde_json::Value) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/issues/{}", self.base_url, workspace, repo_slug, issue_id); let req = self.client.put(&url).json(&body); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } pub async fn delete_issue(&self, workspace: &str, repo_slug: &str, issue_id: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/issues/{}", self.base_url, workspace, repo_slug, issue_id); let req = self.client.delete(&url); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } if resp.status() == reqwest::StatusCode::NO_CONTENT { return Ok(serde_json::json!({})); } Ok(resp.json().await?) } // --- Pipelines --- pub async fn trigger_pipeline(&self, workspace: &str, repo_slug: &str, body: serde_json::Value) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/pipelines/", self.base_url, workspace, repo_slug); let req = self.client.post(&url).json(&body); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } // --- Projects --- pub async fn create_project(&self, workspace: &str, body: serde_json::Value) -> Result<serde_json::Value> { let url = format!("{}/workspaces/{}/projects", self.base_url, workspace); let req = self.client.post(&url).json(&body); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } pub async fn update_project(&self, workspace: &str, project_key: &str, body: serde_json::Value) -> Result<serde_json::Value> { let url = format!("{}/workspaces/{}/projects/{}", self.base_url, workspace, project_key); let req = self.client.put(&url).json(&body); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } pub async fn delete_project(&self, workspace: &str, project_key: &str) -> Result<serde_json::Value> { let url = format!("{}/workspaces/{}/projects/{}", self.base_url, workspace, project_key); let req = self.client.delete(&url); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } if resp.status() == reqwest::StatusCode::NO_CONTENT { return Ok(serde_json::json!({})); } Ok(resp.json().await?) } // --- Snippets --- pub async fn create_snippet(&self, workspace: &str, body: serde_json::Value) -> Result<serde_json::Value> { let url = format!("{}/snippets/{}", self.base_url, workspace); let req = self.client.post(&url).json(&body); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } Ok(resp.json().await?) } pub async fn delete_snippet(&self, workspace: &str, snippet_id: &str) -> Result<serde_json::Value> { let url = format!("{}/snippets/{}/{}", self.base_url, workspace, snippet_id); let req = self.client.delete(&url); let resp = self.apply_auth(req).send().await?; if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); return Err(anyhow!("Bitbucket API error: {} - {}", status, text)); } if resp.status() == reqwest::StatusCode::NO_CONTENT { return Ok(serde_json::json!({})); } Ok(resp.json().await?) } // --- Source --- pub async fn get_file_source(&self, workspace: &str, repo_slug: &str, commit: &str, path: &str) -> Result<serde_json::Value> { let url = format!("{}/repositories/{}/{}/src/{}/{}", self.base_url, workspace, repo_slug, commit, path); let req = self.client.get(&url); let resp = self.apply_auth(req).send().await?; Ok(resp.json().await?) } // Add more methods for each Bitbucket REST API group here } // MCP tool trait implementation and registration will be added here #[derive(Clone)] pub struct BitbucketTool; #[tool(tool_box)] impl BitbucketTool { #[tool(description = "Create a bitbucket pull request")] pub async fn create_pullrequest(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] body: serde_json::Value) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.create_pullrequest(&workspace, &repo_slug, body).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("create_pullrequest error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Get bitbucket pull request details")] pub async fn get_pullrequest(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] pr_id: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.get_pullrequest(&workspace, &repo_slug, &pr_id).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("get_pullrequest error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Update a bitbucket pull request")] pub async fn update_pullrequest(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] pr_id: String, #[tool(param)] body: serde_json::Value) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.update_pullrequest(&workspace, &repo_slug, &pr_id, body).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("update_pullrequest error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Approve a bitbucket pull request")] pub async fn approve_pullrequest(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] pr_id: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.approve_pullrequest(&workspace, &repo_slug, &pr_id).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("approve_pullrequest error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Unapprove a bitbucket pull request")] pub async fn unapprove_pullrequest(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] pr_id: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.unapprove_pullrequest(&workspace, &repo_slug, &pr_id).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("unapprove_pullrequest error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Decline a bitbucket pull request")] pub async fn decline_pullrequest(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] pr_id: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.decline_pullrequest(&workspace, &repo_slug, &pr_id).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("decline_pullrequest error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Merge a bitbucket pull request")] pub async fn merge_pullrequest(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] pr_id: String, #[tool(param)] body: Option<serde_json::Value>) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.merge_pullrequest(&workspace, &repo_slug, &pr_id, body).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("merge_pullrequest error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "List bitbucket pull request comments")] pub async fn list_pullrequest_comments(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] pr_id: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.list_pullrequest_comments(&workspace, &repo_slug, &pr_id).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("list_pullrequest_comments error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Add a bitbucket pull request comment")] pub async fn add_pullrequest_comment(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] pr_id: String, #[tool(param)] body: serde_json::Value) -> Result<CallToolResult, McpError> { let payload = match normalize_comment_input(body) { Ok(p) => p, Err(e) => return Ok(CallToolResult::error(vec![Content::text(e)])), }; let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.add_pullrequest_comment(&workspace, &repo_slug, &pr_id, payload).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("add_pullrequest_comment error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "List bitbucket pull request activity")] pub async fn list_pullrequest_activity(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] pr_id: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.list_pullrequest_activity(&workspace, &repo_slug, &pr_id).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("list_pullrequest_activity error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Get bitbucket pull request diff")] pub async fn get_pullrequest_diff(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] pr_id: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.get_pullrequest_diff(&workspace, &repo_slug, &pr_id).await { Ok(val) => Ok(CallToolResult::success(vec![Content::text(val)])), Err(e) => { tracing::error!("get_pullrequest_diff error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Get bitbucket pull request commits")] pub async fn list_pullrequest_commits(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] pr_id: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.list_pullrequest_commits(&workspace, &repo_slug, &pr_id).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("list_pullrequest_commits error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "List bitbucket pull request tasks")] pub async fn list_pullrequest_tasks(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] pr_id: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.list_pullrequest_tasks(&workspace, &repo_slug, &pr_id).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("list_pullrequest_tasks error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Add a bitbucket pull request task")] pub async fn add_pullrequest_task(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] pr_id: String, #[tool(param)] body: serde_json::Value) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.add_pullrequest_task(&workspace, &repo_slug, &pr_id, body).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("add_pullrequest_task error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Get bitbucket pull request diffstat")] pub async fn get_pullrequest_diffstat(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] pr_id: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.get_pullrequest_diffstat(&workspace, &repo_slug, &pr_id).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("get_pullrequest_diffstat error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Get bitbucket user info")] pub async fn get_user(&self) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.get_user().await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("get_user error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "List bitbucket workspaces")] pub async fn list_workspaces(&self) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.list_workspaces().await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("list_workspaces error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "List bitbucket repositories in a workspace")] pub async fn list_repositories(&self, #[tool(param)] workspace: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.list_repositories(&workspace).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("list_repositories error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "List bitbucket pull requests for a repository")] pub async fn list_pullrequests(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String) -> Result<CallToolResult, McpError> { tracing::info!("list_pullrequests called with workspace='{}', repo_slug='{}'", workspace, repo_slug); let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(format!("env error: {e}"))])) }, }; let result = client.list_pullrequests(&workspace, &repo_slug).await; match result { Ok(val) => { tracing::info!("list_pullrequests API call succeeded"); Ok(CallToolResult::success(vec![Content::json(val)?])) }, Err(e) => { tracing::error!("list_pullrequests API call error: {e}"); Ok(CallToolResult::error(vec![Content::text(format!("api error: {e}"))])) }, } } #[tool(description = "List bitbucket issues for a repository")] pub async fn list_issues(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.list_issues(&workspace, &repo_slug).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("list_issues error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Get bitbucket workspace details")] pub async fn get_workspace(&self, #[tool(param)] workspace: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.get_workspace(&workspace).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("get_workspace error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Get bitbucket repository details")] pub async fn get_repository(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.get_repository(&workspace, &repo_slug).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("get_repository error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "List bitbucket branches for a repository")] pub async fn list_branches(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.list_branches(&workspace, &repo_slug).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("list_branches error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "List bitbucket tags for a repository")] pub async fn list_tags(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.list_tags(&workspace, &repo_slug).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("list_tags error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "List bitbucket commits for a repository")] pub async fn list_commits(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.list_commits(&workspace, &repo_slug).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("list_commits error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "List bitbucket pipelines for a repository")] pub async fn list_pipelines(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.list_pipelines(&workspace, &repo_slug).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("list_pipelines error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "List bitbucket deployments for a repository")] pub async fn list_deployments(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.list_deployments(&workspace, &repo_slug).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("list_deployments error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "List bitbucket downloads for a repository")] pub async fn list_downloads(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.list_downloads(&workspace, &repo_slug).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("list_downloads error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "List bitbucket webhooks for a repository")] pub async fn list_webhooks(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.list_webhooks(&workspace, &repo_slug).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("list_webhooks error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "List bitbucket snippets for a workspace")] pub async fn list_snippets(&self, #[tool(param)] workspace: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.list_snippets(&workspace).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("list_snippets error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "List bitbucket projects for a workspace")] pub async fn list_projects(&self, #[tool(param)] workspace: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.list_projects(&workspace).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("list_projects error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "List bitbucket branch restrictions for a repository")] pub async fn list_branch_restrictions(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.list_branch_restrictions(&workspace, &repo_slug).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("list_branch_restrictions error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "List bitbucket commit statuses for a commit")] pub async fn list_commit_statuses(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] commit: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.list_commit_statuses(&workspace, &repo_slug, &commit).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("list_commit_statuses error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "List bitbucket users in a workspace")] pub async fn list_users(&self, #[tool(param)] workspace: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.list_users(&workspace).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("list_users error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Create a bitbucket repository in a workspace")] pub async fn create_repository(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] body: serde_json::Value) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.create_repository(&workspace, &repo_slug, body).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("create_repository error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Update a bitbucket repository in a workspace")] pub async fn update_repository(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] body: serde_json::Value) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.update_repository(&workspace, &repo_slug, body).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("update_repository error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Delete a bitbucket repository in a workspace")] pub async fn delete_repository(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.delete_repository(&workspace, &repo_slug).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("delete_repository error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Create a bitbucket branch in a repository")] pub async fn create_branch(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] body: serde_json::Value) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.create_branch(&workspace, &repo_slug, body).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("create_branch error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Delete a bitbucket branch in a repository")] pub async fn delete_branch(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] branch: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.delete_branch(&workspace, &repo_slug, &branch).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("delete_branch error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Get bitbucket branching model")] pub async fn get_branching_model(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.get_branching_model(&workspace, &repo_slug).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("get_branching_model error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Update bitbucket branching model")] pub async fn update_branching_model(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] body: serde_json::Value) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.update_branching_model(&workspace, &repo_slug, body).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("update_branching_model error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Create a bitbucket commit status")] pub async fn create_commit_status(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] commit: String, #[tool(param)] body: serde_json::Value) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.create_commit_status(&workspace, &repo_slug, &commit, body).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("create_commit_status error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Get bitbucket commit details")] pub async fn get_commit(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] commit: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.get_commit(&workspace, &repo_slug, &commit).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("get_commit error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Create a bitbucket deployment")] pub async fn create_deployment(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] body: serde_json::Value) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.create_deployment(&workspace, &repo_slug, body).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("create_deployment error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Create a bitbucket issue")] pub async fn create_issue(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] body: serde_json::Value) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.create_issue(&workspace, &repo_slug, body).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("create_issue error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Update a bitbucket issue")] pub async fn update_issue(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] issue_id: String, #[tool(param)] body: serde_json::Value) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.update_issue(&workspace, &repo_slug, &issue_id, body).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("update_issue error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Delete a bitbucket issue")] pub async fn delete_issue(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] issue_id: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.delete_issue(&workspace, &repo_slug, &issue_id).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("delete_issue error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Trigger a bitbucket pipeline")] pub async fn trigger_pipeline(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] body: serde_json::Value) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.trigger_pipeline(&workspace, &repo_slug, body).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("trigger_pipeline error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Create a bitbucket project in a workspace")] pub async fn create_project(&self, #[tool(param)] workspace: String, #[tool(param)] body: serde_json::Value) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.create_project(&workspace, body).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("create_project error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Update a bitbucket project in a workspace")] pub async fn update_project(&self, #[tool(param)] workspace: String, #[tool(param)] project_key: String, #[tool(param)] body: serde_json::Value) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.update_project(&workspace, &project_key, body).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("update_project error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Delete a bitbucket project in a workspace")] pub async fn delete_project(&self, #[tool(param)] workspace: String, #[tool(param)] project_key: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.delete_project(&workspace, &project_key).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("delete_project error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Create a bitbucket snippet in a workspace")] pub async fn create_snippet(&self, #[tool(param)] workspace: String, #[tool(param)] body: serde_json::Value) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.create_snippet(&workspace, body).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("create_snippet error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Delete a bitbucket snippet in a workspace")] pub async fn delete_snippet(&self, #[tool(param)] workspace: String, #[tool(param)] snippet_id: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.delete_snippet(&workspace, &snippet_id).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("delete_snippet error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } #[tool(description = "Get bitbucket file source from a repository")] pub async fn get_file_source(&self, #[tool(param)] workspace: String, #[tool(param)] repo_slug: String, #[tool(param)] commit: String, #[tool(param)] path: String) -> Result<CallToolResult, McpError> { let client = match super::bitbucket::BitbucketClient::from_env() { Ok(c) => c, Err(e) => { tracing::error!("BitbucketClient::from_env error: {e}"); return Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, }; match client.get_file_source(&workspace, &repo_slug, &commit, &path).await { Ok(val) => Ok(CallToolResult::success(vec![Content::json(val)?])), Err(e) => { tracing::error!("get_file_source error: {e}"); Ok(CallToolResult::error(vec![Content::text(e.to_string())])) }, } } } #[tool(tool_box)] impl ServerHandler for BitbucketTool { fn get_info(&self) -> ServerInfo { ServerInfo { instructions: Some("Bitbucket MCP tool: interact with Bitbucket Cloud REST API. Set BITBUCKET_USERNAME and BITBUCKET_APP_PASSWORD env vars.".into()), capabilities: ServerCapabilities::builder().enable_tools().build(), ..Default::default() } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Ibrahimogod/bitbucket-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server