From 7b971c44cbbe62bff3d111414b363796fec17d8f Mon Sep 17 00:00:00 2001 From: guzmonne Date: Sat, 18 Oct 2025 08:30:16 -0300 Subject: [PATCH 1/6] feat(atlassian): add Jira and Confluence module with API integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add atlassian module with support for Jira and Confluence APIs - Implement Jira issue list command using JQL queries - Implement Confluence page search command using CQL queries - Support API Token authentication via environment variables - Add Basic Auth HTTP client for Atlassian API requests - Parse and convert HTML content to plain text for Confluence The module provides: - atlassian/mod.rs: Core authentication and HTTP client setup - atlassian/jira/mod.rs: Jira operations (list/search issues) - atlassian/confluence/mod.rs: Confluence operations (search pages) Both CLI and MCP interfaces are supported with reusable data functions. 🤖 Generated with Claude Code Co-Authored-By: Claude --- crates/mcptools/src/atlassian/confluence.rs | 213 ++++++++++++++++++++ crates/mcptools/src/atlassian/jira.rs | 176 ++++++++++++++++ crates/mcptools/src/atlassian/mod.rs | 81 ++++++++ 3 files changed, 470 insertions(+) create mode 100644 crates/mcptools/src/atlassian/confluence.rs create mode 100644 crates/mcptools/src/atlassian/jira.rs create mode 100644 crates/mcptools/src/atlassian/mod.rs diff --git a/crates/mcptools/src/atlassian/confluence.rs b/crates/mcptools/src/atlassian/confluence.rs new file mode 100644 index 0000000..90c9301 --- /dev/null +++ b/crates/mcptools/src/atlassian/confluence.rs @@ -0,0 +1,213 @@ +use super::{create_authenticated_client, AtlassianConfig}; +use crate::prelude::{println, *}; +use serde::{Deserialize, Serialize}; + +/// Confluence commands +#[derive(Debug, clap::Subcommand)] +pub enum Commands { + /// Search Confluence pages using CQL + #[clap(name = "search")] + Search(SearchOptions), +} + +/// Options for searching Confluence pages +#[derive(Debug, clap::Args, Serialize, Deserialize, Clone)] +pub struct SearchOptions { + /// CQL query (e.g., "space = SPACE AND text ~ 'keyword'") + #[clap(env = "CONFLUENCE_QUERY")] + query: String, + + /// Maximum number of results to return + #[arg(short, long, default_value = "10")] + limit: usize, + + /// Output as JSON + #[arg(long)] + json: bool, +} + +/// Confluence page response from API +#[derive(Debug, Deserialize, Serialize, Clone)] +struct ConfluencePageResponse { + id: String, + title: String, + #[serde(rename = "type")] + page_type: String, + #[serde(default)] + status: Option, + _links: PageLinks, + #[serde(default)] + body: Option, +} + +/// Links from page response +#[derive(Debug, Deserialize, Serialize, Clone)] +struct PageLinks { + #[serde(default)] + webui: Option, +} + +/// Body content from page +#[derive(Debug, Deserialize, Serialize, Clone)] +struct PageBody { + #[serde(default)] + view: Option, +} + +/// View content (HTML) +#[derive(Debug, Deserialize, Serialize, Clone)] +struct ViewContent { + #[serde(default)] + value: Option, +} + +/// Search response from Confluence API +#[derive(Debug, Deserialize)] +struct ConfluenceSearchResponse { + results: Vec, + #[serde(default)] + size: usize, + #[serde(default, rename = "totalSize")] + total_size: usize, +} + +/// Output structure for a single page +#[derive(Debug, Serialize, Clone)] +pub struct PageOutput { + pub id: String, + pub title: String, + pub page_type: String, + pub url: Option, + pub content: Option, +} + +/// Output structure for search command +#[derive(Debug, Serialize)] +pub struct SearchOutput { + pub pages: Vec, + pub total: usize, +} + +/// Convert HTML content to plain text (simple conversion) +fn html_to_plaintext(html: &str) -> String { + // Simple HTML to text conversion - remove tags and decode entities + let text = html + .replace("
", "\n") + .replace("
", "\n") + .replace("
", "\n") + .replace("

", "") + .replace("

", "\n") + .replace("
", "") + .replace("
", "\n"); + + // Remove HTML tags + let re = regex::Regex::new(r"<[^>]+>").unwrap(); + let cleaned = re.replace_all(&text, ""); + + // Decode HTML entities + let decoded = html_escape::decode_html_entities(&cleaned); + + // Clean up excessive whitespace + decoded + .lines() + .map(|line| line.trim()) + .filter(|line| !line.is_empty()) + .collect::>() + .join("\n") +} + +/// Public data function - used by both CLI and MCP +pub async fn search_pages_data(query: String, limit: usize) -> Result { + let config = AtlassianConfig::from_env()?; + let client = create_authenticated_client(&config)?; + + let url = format!("{}/wiki/api/v2/pages/search", config.base_url); + + let response = client + .get(&url) + .query(&[ + ("cql", query), + ("limit", limit.to_string()), + ("bodyFormat", "view".to_string()), + ]) + .send() + .await + .map_err(|e| eyre!("Failed to send request to Confluence: {}", e))?; + + if !response.status().is_success() { + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + return Err(eyre!("Confluence API error [{}]: {}", status, body)); + } + + let search_response: ConfluenceSearchResponse = response + .json() + .await + .map_err(|e| eyre!("Failed to parse Confluence response: {}", e))?; + + let pages = search_response + .results + .into_iter() + .map(|page| { + let content = page + .body + .as_ref() + .and_then(|b| b.view.as_ref()) + .and_then(|v| v.value.as_ref()) + .map(|html| html_to_plaintext(html)); + + PageOutput { + id: page.id, + title: page.title, + page_type: page.page_type, + url: page._links.webui, + content, + } + }) + .collect(); + + Ok(SearchOutput { + pages, + total: search_response.total_size, + }) +} + +/// Handle the search command +async fn search_handler(options: SearchOptions) -> Result<()> { + let data = search_pages_data(options.query, options.limit).await?; + + if options.json { + println!("{}", serde_json::to_string_pretty(&data)?); + } else { + // Human-readable format + println!("Found {} page(s):\n", data.total); + + if data.pages.is_empty() { + println!("No pages found."); + return Ok(()); + } + + let mut table = crate::prelude::new_table(); + table.add_row(prettytable::row!["Title", "Type", "URL"]); + + for page in data.pages { + let url = page.url.unwrap_or_else(|| "N/A".to_string()); + table.add_row(prettytable::row![page.title, page.page_type, url]); + } + + table.printstd(); + } + + Ok(()) +} + +/// Run Confluence commands +pub async fn run(cmd: Commands, global: crate::Global) -> Result<()> { + if global.verbose { + println!("Running Confluence command..."); + } + + match cmd { + Commands::Search(options) => search_handler(options).await, + } +} diff --git a/crates/mcptools/src/atlassian/jira.rs b/crates/mcptools/src/atlassian/jira.rs new file mode 100644 index 0000000..0513407 --- /dev/null +++ b/crates/mcptools/src/atlassian/jira.rs @@ -0,0 +1,176 @@ +use super::{create_authenticated_client, AtlassianConfig}; +use crate::prelude::{println, *}; +use serde::{Deserialize, Serialize}; + +/// Jira commands +#[derive(Debug, clap::Subcommand)] +pub enum Commands { + /// List Jira issues using JQL + #[clap(name = "list")] + List(ListOptions), +} + +/// Options for listing Jira issues +#[derive(Debug, clap::Args, Serialize, Deserialize, Clone)] +pub struct ListOptions { + /// JQL query (e.g., "project = PROJ AND status = Open") + #[clap(env = "JIRA_QUERY")] + query: String, + + /// Maximum number of results to return + #[arg(short, long, default_value = "10")] + limit: usize, + + /// Output as JSON + #[arg(long)] + json: bool, +} + +/// Jira issue response from API +#[derive(Debug, Deserialize, Serialize, Clone)] +struct JiraIssueResponse { + key: String, + fields: JiraIssueFields, +} + +/// Fields from Jira issue +#[derive(Debug, Deserialize, Serialize, Clone)] +struct JiraIssueFields { + summary: String, + #[serde(default)] + description: Option, + status: JiraStatus, + #[serde(default)] + assignee: Option, +} + +/// Jira status field +#[derive(Debug, Deserialize, Serialize, Clone)] +struct JiraStatus { + name: String, +} + +/// Jira assignee field +#[derive(Debug, Deserialize, Serialize, Clone)] +struct JiraAssignee { + #[serde(rename = "displayName")] + display_name: String, +} + +/// Search response from Jira API +#[derive(Debug, Deserialize)] +struct JiraSearchResponse { + issues: Vec, + total: u64, +} + +/// Output structure for a single issue +#[derive(Debug, Serialize, Clone)] +pub struct IssueOutput { + pub key: String, + pub summary: String, + pub description: Option, + pub status: String, + pub assignee: Option, +} + +/// Output structure for list command +#[derive(Debug, Serialize)] +pub struct ListOutput { + pub issues: Vec, + pub total: usize, +} + +/// Public data function - used by both CLI and MCP +pub async fn list_issues_data(query: String, limit: usize) -> Result { + let config = AtlassianConfig::from_env()?; + let client = create_authenticated_client(&config)?; + + let url = format!("{}/rest/api/3/search", config.base_url); + + let body = serde_json::json!({ + "jql": query, + "maxResults": limit, + "fields": ["summary", "description", "status", "assignee"] + }); + + let response = client + .post(&url) + .json(&body) + .send() + .await + .map_err(|e| eyre!("Failed to send request to Jira: {}", e))?; + + if !response.status().is_success() { + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + return Err(eyre!("Jira API error [{}]: {}", status, body)); + } + + let search_response: JiraSearchResponse = response + .json() + .await + .map_err(|e| eyre!("Failed to parse Jira response: {}", e))?; + + let issues = search_response + .issues + .into_iter() + .map(|issue| IssueOutput { + key: issue.key, + summary: issue.fields.summary, + description: issue.fields.description, + status: issue.fields.status.name, + assignee: issue.fields.assignee.map(|a| a.display_name), + }) + .collect(); + + Ok(ListOutput { + issues, + total: search_response.total as usize, + }) +} + +/// Handle the list command +async fn list_handler(options: ListOptions) -> Result<()> { + let data = list_issues_data(options.query, options.limit).await?; + + if options.json { + println!("{}", serde_json::to_string_pretty(&data)?); + } else { + // Human-readable format + println!("Found {} issue(s):\n", data.total); + + if data.issues.is_empty() { + println!("No issues found."); + return Ok(()); + } + + let mut table = crate::prelude::new_table(); + table.add_row(prettytable::row!["Key", "Summary", "Status", "Assignee"]); + + for issue in data.issues { + let assignee = issue.assignee.unwrap_or_else(|| "Unassigned".to_string()); + table.add_row(prettytable::row![ + issue.key, + issue.summary, + issue.status, + assignee + ]); + } + + table.printstd(); + } + + Ok(()) +} + +/// Run Jira commands +pub async fn run(cmd: Commands, global: crate::Global) -> Result<()> { + if global.verbose { + println!("Running Jira command..."); + } + + match cmd { + Commands::List(options) => list_handler(options).await, + } +} diff --git a/crates/mcptools/src/atlassian/mod.rs b/crates/mcptools/src/atlassian/mod.rs new file mode 100644 index 0000000..b963920 --- /dev/null +++ b/crates/mcptools/src/atlassian/mod.rs @@ -0,0 +1,81 @@ +use crate::prelude::{println, *}; +use serde::{Deserialize, Serialize}; + +pub mod confluence; +pub mod jira; + +/// Atlassian module app - root command +#[derive(Debug, clap::Parser)] +#[command(name = "atlassian")] +#[command(about = "Atlassian (Jira, Confluence) operations")] +pub struct App { + #[command(subcommand)] + pub command: Commands, +} + +#[derive(Debug, clap::Subcommand)] +pub enum Commands { + /// Jira operations + #[clap(subcommand)] + Jira(jira::Commands), + + /// Confluence operations + #[clap(subcommand)] + Confluence(confluence::Commands), +} + +/// Atlassian configuration from environment variables +#[derive(Debug, Clone)] +pub struct AtlassianConfig { + pub base_url: String, + pub email: String, + pub api_token: String, +} + +impl AtlassianConfig { + /// Load configuration from environment variables + pub fn from_env() -> Result { + Ok(Self { + base_url: std::env::var("ATLASSIAN_BASE_URL") + .map_err(|_| eyre!("ATLASSIAN_BASE_URL environment variable not set"))?, + email: std::env::var("ATLASSIAN_EMAIL") + .map_err(|_| eyre!("ATLASSIAN_EMAIL environment variable not set"))?, + api_token: std::env::var("ATLASSIAN_API_TOKEN") + .map_err(|_| eyre!("ATLASSIAN_API_TOKEN environment variable not set"))?, + }) + } +} + +/// Create an authenticated HTTP client with Basic Auth headers +pub fn create_authenticated_client(config: &AtlassianConfig) -> Result { + use base64::Engine; + use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}; + + let auth_string = format!("{}:{}", config.email, config.api_token); + let auth_encoded = base64::engine::general_purpose::STANDARD.encode(&auth_string); + + let mut headers = HeaderMap::new(); + headers.insert( + AUTHORIZATION, + HeaderValue::from_str(&format!("Basic {}", auth_encoded)) + .map_err(|e| eyre!("Invalid header value: {}", e))?, + ); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + + reqwest::Client::builder() + .default_headers(headers) + .build() + .map_err(|e| eyre!("Failed to build HTTP client: {}", e)) +} + +/// Module entry point +pub async fn run(app: App, global: crate::Global) -> Result<()> { + if global.verbose { + println!("Running Atlassian module..."); + } + + match app.command { + Commands::Jira(cmd) => jira::run(cmd, global).await, + Commands::Confluence(cmd) => confluence::run(cmd, global).await, + } +} From 945bf833614e7668e6c29f9dea4b4bad557c1305 Mon Sep 17 00:00:00 2001 From: guzmonne Date: Sat, 18 Oct 2025 19:08:11 -0300 Subject: [PATCH 2/6] feat(mcp): add Atlassian tools for Jira and Confluence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add jira_list MCP tool for searching Jira issues via JQL - Add confluence_search MCP tool for searching Confluence pages via CQL - Implement proper MCP request/response handling - Parse tool arguments and handle errors gracefully - Return formatted JSON output for LLM consumption Exposes Jira and Confluence functionality through the Model Context Protocol, enabling Claude and other LLM agents to integrate with Atlassian systems. 🤖 Generated with Claude Code Co-Authored-By: Claude --- crates/mcptools/src/mcp/tools/atlassian.rs | 113 +++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 crates/mcptools/src/mcp/tools/atlassian.rs diff --git a/crates/mcptools/src/mcp/tools/atlassian.rs b/crates/mcptools/src/mcp/tools/atlassian.rs new file mode 100644 index 0000000..dc770a5 --- /dev/null +++ b/crates/mcptools/src/mcp/tools/atlassian.rs @@ -0,0 +1,113 @@ +use crate::prelude::{eprintln, *}; +use serde::Deserialize; + +use super::{CallToolResult, Content, JsonRpcError}; + +/// Handle Jira list command via MCP +pub async fn handle_jira_list( + arguments: Option, + global: &crate::Global, +) -> Result { + #[derive(Deserialize)] + struct JiraListArgs { + query: String, + limit: Option, + } + + let args: JiraListArgs = serde_json::from_value(arguments.unwrap_or(serde_json::Value::Null)) + .map_err(|e| JsonRpcError { + code: -32602, + message: format!("Invalid arguments: {e}"), + data: None, + })?; + + if global.verbose { + eprintln!( + "Calling jira_list: query={}, limit={:?}", + args.query, args.limit + ); + } + + // Call the Jira module's data function + let list_data = crate::atlassian::jira::list_issues_data(args.query, args.limit.unwrap_or(10)) + .await + .map_err(|e| JsonRpcError { + code: -32603, + message: format!("Tool execution error: {e}"), + data: None, + })?; + + // Convert to JSON and wrap in MCP result format + let json_string = serde_json::to_string_pretty(&list_data).map_err(|e| JsonRpcError { + code: -32603, + message: format!("Serialization error: {e}"), + data: None, + })?; + + let result = CallToolResult { + content: vec![Content::Text { text: json_string }], + is_error: None, + }; + + serde_json::to_value(result).map_err(|e| JsonRpcError { + code: -32603, + message: format!("Internal error: {e}"), + data: None, + }) +} + +/// Handle Confluence search command via MCP +pub async fn handle_confluence_search( + arguments: Option, + global: &crate::Global, +) -> Result { + #[derive(Deserialize)] + struct ConfluenceSearchArgs { + query: String, + limit: Option, + } + + let args: ConfluenceSearchArgs = + serde_json::from_value(arguments.unwrap_or(serde_json::Value::Null)).map_err(|e| { + JsonRpcError { + code: -32602, + message: format!("Invalid arguments: {e}"), + data: None, + } + })?; + + if global.verbose { + eprintln!( + "Calling confluence_search: query={}, limit={:?}", + args.query, args.limit + ); + } + + // Call the Confluence module's data function + let search_data = + crate::atlassian::confluence::search_pages_data(args.query, args.limit.unwrap_or(10)) + .await + .map_err(|e| JsonRpcError { + code: -32603, + message: format!("Tool execution error: {e}"), + data: None, + })?; + + // Convert to JSON and wrap in MCP result format + let json_string = serde_json::to_string_pretty(&search_data).map_err(|e| JsonRpcError { + code: -32603, + message: format!("Serialization error: {e}"), + data: None, + })?; + + let result = CallToolResult { + content: vec![Content::Text { text: json_string }], + is_error: None, + }; + + serde_json::to_value(result).map_err(|e| JsonRpcError { + code: -32603, + message: format!("Internal error: {e}"), + data: None, + }) +} From 0649ca4a8387223d38f75a7aeac67005db7ba130 Mon Sep 17 00:00:00 2001 From: guzmonne Date: Sat, 18 Oct 2025 19:09:34 -0300 Subject: [PATCH 3/6] feat(cli): integrate Atlassian module into main application MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add atlassian module declaration and SubCommand variant - Add Atlassian environment variables to Global config - ATLASSIAN_BASE_URL: Base URL for Atlassian instance - ATLASSIAN_EMAIL: User email for API authentication - ATLASSIAN_API_TOKEN: API token for authentication - Register jira_list and confluence_search MCP tools - Update tools list with Atlassian tool definitions and schemas - Route tool calls to appropriate handlers Enables full CLI and MCP integration of Atlassian functionality. 🤖 Generated with Claude Code Co-Authored-By: Claude --- crates/mcptools/src/main.rs | 17 ++++++++++++ crates/mcptools/src/mcp/tools/mod.rs | 39 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/crates/mcptools/src/main.rs b/crates/mcptools/src/main.rs index 750647e..19799f1 100644 --- a/crates/mcptools/src/main.rs +++ b/crates/mcptools/src/main.rs @@ -3,6 +3,7 @@ use crate::prelude::*; use clap::Parser; +mod atlassian; mod error; mod hn; mod mcp; @@ -36,10 +37,25 @@ pub struct Global { /// Whether to display additional information. #[clap(long, env = "YAWNS_VERBOSE", global = true, default_value = "false")] verbose: bool, + + /// Atlassian base URL (e.g., https://your-domain.atlassian.net) + #[clap(long, env = "ATLASSIAN_BASE_URL", global = true)] + pub atlassian_url: Option, + + /// Atlassian email + #[clap(long, env = "ATLASSIAN_EMAIL", global = true)] + pub atlassian_email: Option, + + /// Atlassian API token + #[clap(long, env = "ATLASSIAN_API_TOKEN", global = true)] + pub atlassian_token: Option, } #[derive(Debug, clap::Parser)] pub enum SubCommands { + /// Atlassian (Jira, Confluence) operations + Atlassian(crate::atlassian::App), + /// HackerNews (news.ycombinator.com) operations HN(crate::hn::App), @@ -58,6 +74,7 @@ async fn main() -> Result<()> { let app = App::parse(); match app.command { + SubCommands::Atlassian(sub_app) => crate::atlassian::run(sub_app, app.global).await, SubCommands::HN(sub_app) => crate::hn::run(sub_app, app.global).await, SubCommands::MCP(sub_app) => crate::mcp::run(sub_app, app.global).await, SubCommands::MD(sub_app) => crate::md::run(sub_app, app.global).await, diff --git a/crates/mcptools/src/mcp/tools/mod.rs b/crates/mcptools/src/mcp/tools/mod.rs index 44b3f25..de3847e 100644 --- a/crates/mcptools/src/mcp/tools/mod.rs +++ b/crates/mcptools/src/mcp/tools/mod.rs @@ -1,3 +1,4 @@ +mod atlassian; mod hn; mod md; @@ -76,6 +77,42 @@ pub fn handle_initialize() -> Result { pub fn handle_tools_list() -> Result { let tools = vec![ + Tool { + name: "jira_list".to_string(), + description: "Search Jira issues using JQL (Jira Query Language). Returns a list of issues matching the query with details like key, summary, status, and assignee. Requires ATLASSIAN_BASE_URL, ATLASSIAN_EMAIL, and ATLASSIAN_API_TOKEN environment variables.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "JQL query to search issues (e.g., 'project = PROJ AND status = Open')" + }, + "limit": { + "type": "number", + "description": "Maximum number of results to return (default: 10)" + } + }, + "required": ["query"] + }), + }, + Tool { + name: "confluence_search".to_string(), + description: "Search Confluence pages using CQL (Confluence Query Language). Returns a list of pages matching the query with title, type, URL, and optionally the plain text content. Requires ATLASSIAN_BASE_URL, ATLASSIAN_EMAIL, and ATLASSIAN_API_TOKEN environment variables.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "CQL query to search pages (e.g., 'space = SPACE AND text ~ \"keyword\"')" + }, + "limit": { + "type": "number", + "description": "Maximum number of results to return (default: 10)" + } + }, + "required": ["query"] + }), + }, Tool { name: "hn_read_item".to_string(), description: "Read a HackerNews post and its comments. Accepts HackerNews item ID (e.g., '8863') or full URL (e.g., 'https://news.ycombinator.com/item?id=8863'). Returns post details with paginated comments.".to_string(), @@ -231,6 +268,8 @@ pub async fn handle_tools_call( })?; match params.name.as_str() { + "jira_list" => atlassian::handle_jira_list(params.arguments, global).await, + "confluence_search" => atlassian::handle_confluence_search(params.arguments, global).await, "hn_read_item" => hn::handle_hn_read_item(params.arguments, global).await, "hn_list_items" => hn::handle_hn_list_items(params.arguments, global).await, "md_fetch" => md::handle_md_fetch(params.arguments, global).await, From 8590e37c8d7df1a69a19e65d447800c7a161e6a2 Mon Sep 17 00:00:00 2001 From: guzmonne Date: Sat, 18 Oct 2025 19:09:59 -0300 Subject: [PATCH 4/6] build: add dependencies for Atlassian module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add to workspace and crate dependencies: - base64 (v0.21.7): For Basic Authentication encoding - html-escape (v0.2.13): For HTML entity decoding in Confluence content These dependencies enable API authentication and content parsing required by the Atlassian module for Jira and Confluence integration. 🤖 Generated with Claude Code Co-Authored-By: Claude --- Cargo.lock | 31 +++++++++++++++++++++++++++---- Cargo.toml | 2 ++ crates/mcptools/Cargo.toml | 2 ++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index db3c7ec..e7b0219 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -197,6 +197,12 @@ dependencies = [ "windows-link 0.2.0", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -926,7 +932,7 @@ checksum = "f77a421a200d6314c8830919715d8452320c16e06b37686b13a9942f799dbf9b" dependencies = [ "anyhow", "auto_generate_cdp", - "base64", + "base64 0.22.1", "derive_builder", "log", "rand 0.9.2", @@ -953,6 +959,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + [[package]] name = "html2md" version = "0.2.15" @@ -1088,7 +1103,7 @@ version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1459,6 +1474,7 @@ version = "0.2.0" dependencies = [ "anstream", "axum", + "base64 0.21.7", "chrono", "clap", "color-eyre", @@ -1466,6 +1482,7 @@ dependencies = [ "env_logger", "futures", "headless_chrome", + "html-escape", "html2md", "prettytable", "regex", @@ -1951,7 +1968,7 @@ version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -2740,7 +2757,7 @@ version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" dependencies = [ - "base64", + "base64 0.22.1", "flate2", "log", "once_cell", @@ -2769,6 +2786,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + [[package]] name = "utf8_iter" version = "1.0.4" diff --git a/Cargo.toml b/Cargo.toml index 9e34c86..a4f4140 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,3 +36,5 @@ colored = "3.0.0" headless_chrome = "1.0.18" html2md = "0.2.15" scraper = "0.20" +base64 = "0.21.7" +html-escape = "0.2.13" diff --git a/crates/mcptools/Cargo.toml b/crates/mcptools/Cargo.toml index 623e6d9..2e50d45 100644 --- a/crates/mcptools/Cargo.toml +++ b/crates/mcptools/Cargo.toml @@ -33,3 +33,5 @@ colored = { workspace = true } headless_chrome = { workspace = true } html2md = { workspace = true } scraper = { workspace = true } +base64 = { workspace = true } +html-escape = { workspace = true } From 7d96751d52a0d66f8412ae4bde462b7e367e22c2 Mon Sep 17 00:00:00 2001 From: guzmonne Date: Sat, 18 Oct 2025 19:10:09 -0300 Subject: [PATCH 5/6] docs(atlassian): add comprehensive setup and quick start guides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two documentation files: 1. ATLASSIAN_SETUP.md: - Step-by-step guide for API token generation - 4 methods to configure environment variables - Verification and testing procedures - Usage examples for Jira and Confluence - Troubleshooting section with common errors - Security best practices - Links to official Atlassian documentation 2. ATLASSIAN_QUICK_START.md: - 5-minute setup checklist - 15+ common command examples - Query language reference (JQL and CQL) - jq filtering examples - Error troubleshooting table - Tips and tricks for power users Provides users with both detailed and quick reference documentation for configuring and using the Atlassian module with Jira and Confluence. 🤖 Generated with Claude Code Co-Authored-By: Claude --- ATLASSIAN_QUICK_START.md | 217 +++++++++++++++++++++++++++++++++++ ATLASSIAN_SETUP.md | 242 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 459 insertions(+) create mode 100644 ATLASSIAN_QUICK_START.md create mode 100644 ATLASSIAN_SETUP.md diff --git a/ATLASSIAN_QUICK_START.md b/ATLASSIAN_QUICK_START.md new file mode 100644 index 0000000..1cc8b17 --- /dev/null +++ b/ATLASSIAN_QUICK_START.md @@ -0,0 +1,217 @@ +# Atlassian Module - Quick Start Guide + +## 5-Minute Setup + +### 1. Generate API Token +- Go to https://id.atlassian.com/manage-profile/security/api-tokens +- Click "Create API token" +- Copy the generated token + +### 2. Set Environment Variables +```bash +export ATLASSIAN_BASE_URL="https://your-domain.atlassian.net" +export ATLASSIAN_EMAIL="your-email@company.com" +export ATLASSIAN_API_TOKEN="your-api-token-here" +``` + +### 3. Test Configuration +```bash +mcptools atlassian jira list "project IS NOT EMPTY" --limit 5 +``` + +--- + +## Common Jira Queries + +### View Your Assigned Issues +```bash +mcptools atlassian jira list "assignee = currentUser() AND status != Done" +``` + +### Find Open Issues in a Project +```bash +mcptools atlassian jira list "project = PROJ AND status = Open" +``` + +### Search Issues by Text +```bash +mcptools atlassian jira list "text ~ 'database' AND type = Bug" +``` + +### Recently Updated Issues +```bash +mcptools atlassian jira list "updated >= -7d ORDER BY updated DESC" +``` + +### High Priority Issues +```bash +mcptools atlassian jira list "priority = High AND status NOT IN (Done, Closed)" +``` + +### Get JSON Output (for scripting) +```bash +mcptools atlassian jira list "project = PROJ" --json | jq '.issues[] | {key, summary, status}' +``` + +--- + +## Common Confluence Queries + +### Find Pages About a Topic +```bash +mcptools atlassian confluence search "text ~ 'deployment'" +``` + +### Search in a Specific Space +```bash +mcptools atlassian confluence search "space = WIKI AND text ~ 'api'" +``` + +### Recent Pages +```bash +mcptools atlassian confluence search "lastModified >= -30d ORDER BY lastModified DESC" +``` + +### Get JSON Output +```bash +mcptools atlassian confluence search "text ~ 'guide'" --json | jq '.pages[] | {title, page_type, url}' +``` + +### Limit Results +```bash +mcptools atlassian confluence search "type = page" --limit 20 +``` + +--- + +## Tips & Tricks + +### Save Results to File +```bash +mcptools atlassian jira list "project = PROJ" --json > issues.json +``` + +### Filter JSON Results with jq +```bash +# Get only open issues +mcptools atlassian jira list "project = PROJ" --json | jq '.issues[] | select(.status == "Open")' + +# Count issues by status +mcptools atlassian jira list "project = PROJ" --json | jq '.issues | group_by(.status) | map({status: .[0].status, count: length})' +``` + +### Use Verbose Mode for Debugging +```bash +mcptools --verbose atlassian jira list "project = PROJ" +``` + +### Combine with Other Tools +```bash +# Search and pipe to grep +mcptools atlassian jira list "project = PROJ" | grep -i "database" + +# Count results +mcptools atlassian jira list "project = PROJ" --json | jq '.issues | length' +``` + +--- + +## CLI Argument Options + +### Jira List Command +``` +mcptools atlassian jira list [OPTIONS] + +Arguments: + JQL query string + +Options: + -l, --limit Max results [default: 10] + --json Output as JSON + --help Show help +``` + +### Confluence Search Command +``` +mcptools atlassian confluence search [OPTIONS] + +Arguments: + CQL query string + +Options: + -l, --limit Max results [default: 10] + --json Output as JSON + --help Show help +``` + +### Global Options +``` +--atlassian-url Override ATLASSIAN_BASE_URL env var +--atlassian-email Override ATLASSIAN_EMAIL env var +--atlassian-token Override ATLASSIAN_API_TOKEN env var +--verbose Enable verbose logging +``` + +--- + +## Query Language Reference + +### JQL (Jira Query Language) + +**Common Fields:** +- `project` - Project key (e.g., `project = PROJ`) +- `status` - Issue status (e.g., `status = Open`) +- `assignee` - Who it's assigned to (e.g., `assignee = currentUser()`) +- `text` - Full text search (e.g., `text ~ 'keyword'`) +- `type` - Issue type (e.g., `type = Bug`) +- `priority` - Priority level (e.g., `priority = High`) +- `updated` - Last update date (e.g., `updated >= -7d`) +- `created` - Creation date (e.g., `created >= -30d`) + +**Operators:** +- `=` - Equals +- `!=` - Not equals +- `~` - Contains (text search) +- `>`, `<`, `>=`, `<=` - Comparison +- `IN` - One of multiple values +- `NOT IN` - None of values +- `AND` - Combine conditions +- `OR` - Either condition + +### CQL (Confluence Query Language) + +**Common Fields:** +- `space` - Space key (e.g., `space = WIKI`) +- `text` - Full text search (e.g., `text ~ 'keyword'`) +- `type` - Page type (e.g., `type = page`) +- `lastModified` - Last modification date (e.g., `lastModified >= -30d`) +- `creator` - Page creator +- `title` - Page title + +**Operators:** Similar to JQL + +--- + +## Common Errors & Solutions + +| Error | Cause | Solution | +|-------|-------|----------| +| "ATLASSIAN_BASE_URL environment variable not set" | Missing env var | `export ATLASSIAN_BASE_URL="..."` | +| "Jira API error [401]" | Invalid credentials | Check email and API token | +| "Jira API error [403]" | Insufficient permissions | Check user account permissions | +| "Jira API error [404]" | Wrong base URL or project | Verify base URL and project key | +| Connection timeout | Network issue | Check internet and URL accessibility | + +--- + +## More Information + +For detailed setup instructions, see: [ATLASSIAN_SETUP.md](ATLASSIAN_SETUP.md) + +For MCP usage, see: [CLAUDE.md](CLAUDE.md#atlassian-configuration) + +For API documentation: +- [Jira Cloud REST API v3](https://developer.atlassian.com/cloud/jira/rest/v3) +- [Confluence Cloud REST API v2](https://developer.atlassian.com/cloud/confluence/rest/v2) +- [JQL Reference](https://support.atlassian.com/jira-software-cloud/docs/advanced-searching-using-jql/) +- [CQL Reference](https://support.atlassian.com/confluence-cloud/docs/advanced-searching-using-cql/) diff --git a/ATLASSIAN_SETUP.md b/ATLASSIAN_SETUP.md new file mode 100644 index 0000000..27b2f0f --- /dev/null +++ b/ATLASSIAN_SETUP.md @@ -0,0 +1,242 @@ +# Atlassian Module Configuration Guide + +This guide explains how to set up and configure credentials to use the `atlassian` commands for Jira and Confluence. + +## Prerequisites + +- Access to an Atlassian Cloud instance (Jira and/or Confluence) +- Your Atlassian email address +- Administrator or user account with appropriate permissions + +## Step 1: Generate an Atlassian API Token + +### For Jira Cloud and Confluence Cloud + +1. **Log in to your Atlassian account:** + - Go to https://id.atlassian.com/manage-profile/security/api-tokens + +2. **Create a new API token:** + - Click "Create API token" + - Give it a descriptive name (e.g., "mcptools") + - Click "Create" + - Copy the generated token (you won't be able to see it again) + +### Important Notes + +- API tokens are **long-lived credentials** (never expire unless manually revoked) +- **Treat them like passwords** - never commit them to version control +- You can revoke tokens anytime if they're compromised +- For security, create a separate token for mcptools rather than using a personal token + +## Step 2: Get Your Atlassian Base URL + +Your base URL depends on where your Atlassian instance is hosted: + +- **Atlassian Cloud (most common):** `https://your-domain.atlassian.net` + - Example: `https://mycompany.atlassian.net` +- **Self-hosted:** `https://your-jira-server.com` + +To find your URL: +- Open your Jira or Confluence instance in a browser +- Look at the URL bar - extract the base domain +- Example: If your Jira URL is `https://mycompany.atlassian.net/browse/PROJ-123`, your base URL is `https://mycompany.atlassian.net` + +## Step 3: Configure Environment Variables + +The `atlassian` commands require three environment variables: + +### Option A: Set in Your Shell Profile (Recommended) + +1. **Edit your shell configuration file:** + - For bash: `~/.bashrc` or `~/.bash_profile` + - For zsh: `~/.zshrc` + - For fish: `~/.config/fish/config.fish` + +2. **Add the following lines:** + + ```bash + export ATLASSIAN_BASE_URL="https://your-domain.atlassian.net" + export ATLASSIAN_EMAIL="your-email@company.com" + export ATLASSIAN_API_TOKEN="your-api-token-here" + ``` + +3. **Reload your shell:** + ```bash + source ~/.bashrc # or appropriate file for your shell + ``` + +### Option B: Set for Current Session Only + +```bash +export ATLASSIAN_BASE_URL="https://your-domain.atlassian.net" +export ATLASSIAN_EMAIL="your-email@company.com" +export ATLASSIAN_API_TOKEN="your-api-token-here" +``` + +### Option C: Use a .env File (Development Only) + +Create a `.env` file in your project root: + +```bash +ATLASSIAN_BASE_URL=https://your-domain.atlassian.net +ATLASSIAN_EMAIL=your-email@company.com +ATLASSIAN_API_TOKEN=your-api-token-here +``` + +Then load it before running commands: + +```bash +source .env +mcptools atlassian jira list "project = PROJ" +``` + +### Option D: Pass as Command-Line Arguments + +```bash +mcptools \ + --atlassian-url "https://your-domain.atlassian.net" \ + --atlassian-email "your-email@company.com" \ + --atlassian-token "your-api-token-here" \ + atlassian jira list "project = PROJ" +``` + +## Step 4: Verify Configuration + +Test your configuration with a simple Jira query: + +```bash +mcptools atlassian jira list "project IS NOT EMPTY" --limit 5 +``` + +Expected output (if successful): +``` +Found N issue(s): + ++----------+-------------------+--------+----------+ +| Key | Summary | Status | Assignee | ++==========+===================+========+==========+ +| PROJ-123 | Issue title | Open | John Doe | ++----------+-------------------+--------+----------+ +... +``` + +If you get an error like `ATLASSIAN_BASE_URL environment variable not set`, ensure all three environment variables are correctly configured. + +## Usage Examples + +### Jira Commands + +**List open issues in a project:** +```bash +mcptools atlassian jira list "project = PROJ AND status = Open" +``` + +**List issues assigned to you:** +```bash +mcptools atlassian jira list "assignee = currentUser()" +``` + +**Search with JQL and limit results:** +```bash +mcptools atlassian jira list "text ~ 'database' AND status = 'In Progress'" --limit 20 +``` + +**Output as JSON:** +```bash +mcptools atlassian jira list "project = PROJ" --json | jq '.issues[] | {key, summary, status}' +``` + +### Confluence Commands + +**Search for pages about a topic:** +```bash +mcptools atlassian confluence search "text ~ 'deployment'" +``` + +**Search in a specific space:** +```bash +mcptools atlassian confluence search "space = WIKI AND text ~ 'guide'" +``` + +**Limit results and output as JSON:** +```bash +mcptools atlassian confluence search "text ~ 'api'" --limit 5 --json +``` + +## Using with MCP (Claude) + +The Atlassian module is also available as MCP tools that Claude can use: + +- **`jira_list`** - Search Jira issues using JQL +- **`confluence_search`** - Search Confluence pages using CQL + +These tools are automatically available when using mcptools as an MCP server. + +## Troubleshooting + +### Error: "ATLASSIAN_BASE_URL environment variable not set" + +**Solution:** Make sure all three environment variables are set: +```bash +echo $ATLASSIAN_BASE_URL +echo $ATLASSIAN_EMAIL +echo $ATLASSIAN_API_TOKEN +``` + +If any are missing, configure them using one of the methods above. + +### Error: "Jira API error [401]: ..." + +**Solution:** Your authentication credentials are incorrect. Check: +- API token is correct (copy it again from https://id.atlassian.com/manage-profile/security/api-tokens) +- Email address matches the one associated with the token +- Base URL is correct + +### Error: "Jira API error [403]: ..." + +**Solution:** Your user account doesn't have permission to perform this action. Check: +- Your account has permission to view the projects/issues you're querying +- Your token has appropriate scopes (should have `read:jira-work` and `search:jira`) + +### Error: "Jira API error [404]: ..." + +**Solution:** The resource doesn't exist or your base URL is incorrect. Verify: +- Base URL is correct (should NOT include `/browse` or `/wiki`) +- The project or issue exists + +### Connection Timeouts + +If commands are timing out: +- Check your internet connection +- Verify your Atlassian instance is accessible from your network +- Try with a simpler query to isolate the issue + +## Security Best Practices + +1. **Never commit credentials to version control:** + - Add `.env` to your `.gitignore` + - Don't hardcode tokens in scripts + +2. **Use environment variables:** + - Keep tokens out of command history + - Use shell profiles to auto-load on session start + +3. **Rotate tokens regularly:** + - Review and revoke old tokens + - Create new tokens for different use cases + +4. **Limit token scope:** + - Only grant necessary permissions + - Atlassian API tokens have broad permissions by default + +5. **Monitor token usage:** + - Check Atlassian's security log for token usage + - Revoke tokens immediately if compromised + +## Additional Resources + +- [Atlassian API Token Documentation](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/) +- [Jira Cloud REST API Documentation](https://developer.atlassian.com/cloud/jira/rest/v3) +- [Confluence Cloud REST API Documentation](https://developer.atlassian.com/cloud/confluence/rest/v2) +- [Jira Query Language (JQL) Documentation](https://support.atlassian.com/jira-software-cloud/docs/advanced-searching-using-jql/) +- [Confluence Query Language (CQL) Documentation](https://support.atlassian.com/confluence-cloud/docs/advanced-searching-using-cql/) From 0a1ebcc1a186be07b627870f6dd6554e0b438beb Mon Sep 17 00:00:00 2001 From: guzmonne Date: Sat, 18 Oct 2025 19:11:02 -0300 Subject: [PATCH 6/6] docs(claude): update project documentation for Atlassian module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update CLAUDE.md with: - Include Atlassian tools in project overview - Document Atlassian module structure and files - Add Atlassian configuration section with env vars - Reference ATLASSIAN_SETUP.md for detailed instructions - Update Code Architecture section with new module details Ensures developers are aware of Atlassian functionality and know where to find setup and configuration information. 🤖 Generated with Claude Code Co-Authored-By: Claude --- CLAUDE.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 39ffc13..e47dcaa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -MCPTOOLS is a Rust workspace providing MCP (Model Context Protocol) exposed tools for LLM Coding Agents. Currently implements AWS shortcuts, with primary focus on KMS (Key Management Service) operations. +MCPTOOLS is a Rust workspace providing MCP (Model Context Protocol) exposed tools for LLM Coding Agents. Currently implements: +- AWS shortcuts (KMS operations) +- Atlassian tools (Jira and Confluence search and management) +- Web content fetching and parsing ## Workspace Structure @@ -108,10 +111,14 @@ cargo xtask dev-docs The application uses a modular CLI structure with clap for argument parsing: -- `main.rs` - Entry point, CLI app structure with global options (AWS region, profile, verbose) +- `main.rs` - Entry point, CLI app structure with global options (AWS region, profile, verbose, Atlassian credentials) - `prelude.rs` - Common imports and utilities (Result type, logging macros, table formatting) - `error.rs` - Custom error types using thiserror -- `kms.rs` - AWS KMS operations (list keys, get policies) +- `atlassian/` - Atlassian (Jira/Confluence) operations + - `mod.rs` - Authentication config and HTTP client setup + - `jira/mod.rs` - Jira operations (list/search issues) + - `confluence/mod.rs` - Confluence operations (search pages) +- `mcp/tools/atlassian.rs` - MCP tool handlers for Jira and Confluence **CLI Structure:** @@ -150,6 +157,16 @@ The application respects standard AWS configuration: - Default region: `us-east-1` - Default profile: `default` +### Atlassian Configuration + +The Atlassian module requires three environment variables: + +- `ATLASSIAN_BASE_URL` (e.g., `https://your-domain.atlassian.net`) +- `ATLASSIAN_EMAIL` (your Atlassian email) +- `ATLASSIAN_API_TOKEN` (API token from https://id.atlassian.com/manage-profile/security/api-tokens) + +See `ATLASSIAN_SETUP.md` for detailed setup instructions. + ### Adding New AWS Services To add a new AWS service (e.g., S3):