jira_mcp_server/
lib.rs

1//! JIRA MCP Server Library
2//!
3//! An AI-friendly JIRA integration server using the Model Context Protocol (MCP).
4//! This server provides semantic tools for searching, retrieving, commenting on, and analyzing
5//! relationships between JIRA issues without requiring knowledge of JQL or JIRA internals.
6//!
7//! ## Features
8//!
9//! - **AI-Friendly Interface**: Uses semantic parameters instead of JQL
10//! - **Real JIRA API Integration**: Leverages gouqi 0.14.0 for Cloud/Server operations
11//! - **Smart Caching**: Metadata caching with TTL for performance
12//! - **Comprehensive Tools**: Search, issue details, user issues, commenting, relationship analysis
13//! - **Issue Interaction**: Add comments and analyze issue relationship graphs
14//! - **Error Handling**: MCP-compliant error codes and messages
15
16use crate::cache::{MetadataCache, UserMapping};
17use crate::config::JiraConfig;
18use crate::error::{JiraMcpError, JiraMcpResult};
19use crate::jira_client::JiraClient;
20use crate::tools::{
21    AddCommentParams, AddCommentResult, AddCommentTool, AddTodoParams, AddTodoResult,
22    AssignIssueParams, AssignIssueResult, AssignIssueTool, BulkAddLabelsParams,
23    BulkAddLabelsResult, BulkAssignIssuesParams, BulkAssignIssuesResult, BulkCreateIssuesParams,
24    BulkCreateIssuesResult, BulkOperationsTool, BulkTransitionIssuesParams,
25    BulkTransitionIssuesResult, BulkUpdateFieldsParams, BulkUpdateFieldsResult,
26    CancelTodoWorkParams, CancelTodoWorkResult, CheckpointTodoWorkParams, CheckpointTodoWorkResult,
27    CloseSprintParams, CloseSprintResult, CloseSprintTool, CompleteTodoWorkParams,
28    CompleteTodoWorkResult, ComponentsTool, CreateIssueParams, CreateIssueResult, CreateIssueTool,
29    CreateSprintParams, CreateSprintResult, CreateSprintTool, DeleteIssueLinkParams,
30    DeleteIssueLinkResult, DeleteIssueLinkTool, DownloadAttachmentParams, DownloadAttachmentResult,
31    DownloadAttachmentTool, GetActiveWorkSessionsResult, GetAvailableComponentsParams,
32    GetAvailableComponentsResult, GetAvailableLabelsParams, GetAvailableLabelsResult,
33    GetAvailableTransitionsParams, GetAvailableTransitionsResult, GetAvailableTransitionsTool,
34    GetCreateMetadataParams, GetCreateMetadataResult, GetCreateMetadataTool, GetCustomFieldsParams,
35    GetCustomFieldsResult, GetCustomFieldsTool, GetIssueDetailsParams, GetIssueDetailsResult,
36    GetIssueDetailsTool, GetIssueLinkTypesResult, GetIssueLinkTypesTool, GetSprintInfoParams,
37    GetSprintInfoResult, GetSprintInfoTool, GetSprintIssuesParams, GetSprintIssuesResult,
38    GetSprintIssuesTool, GetUserIssuesParams, GetUserIssuesResult, GetUserIssuesTool,
39    IssueRelationshipsParams, IssueRelationshipsResult, IssueRelationshipsTool, LabelsTool,
40    LinkIssuesParams, LinkIssuesResult, LinkIssuesTool, ListAttachmentsParams,
41    ListAttachmentsResult, ListAttachmentsTool, ListSprintsParams, ListSprintsResult,
42    ListSprintsTool, ListTodosParams, ListTodosResult, ManageLabelsParams, ManageLabelsResult,
43    MoveToSprintParams, MoveToSprintResult, MoveToSprintTool, PauseTodoWorkParams,
44    PauseTodoWorkResult, SearchIssuesParams, SearchIssuesResult, SearchIssuesTool,
45    SetTodoBaseParams, SetTodoBaseResult, StartSprintParams, StartSprintResult, StartSprintTool,
46    StartTodoWorkParams, StartTodoWorkResult, TodoTracker, TransitionIssueParams,
47    TransitionIssueResult, TransitionIssueTool, UpdateComponentsParams, UpdateComponentsResult,
48    UpdateCustomFieldsParams, UpdateCustomFieldsResult, UpdateCustomFieldsTool, UpdateDescription,
49    UpdateDescriptionParams, UpdateDescriptionResult, UpdateTodoParams, UpdateTodoResult,
50    UploadAttachmentParams, UploadAttachmentResult, UploadAttachmentTool,
51};
52
53use pulseengine_mcp_macros::{mcp_server, mcp_tools};
54use serde::{Deserialize, Serialize};
55use std::sync::Arc;
56use std::time::Instant;
57use tracing::{error, info, instrument, warn};
58
59// Re-export modules for external use
60pub mod cache;
61pub mod config;
62pub mod error;
63pub mod jira_client;
64pub mod semantic_mapping;
65pub mod tools;
66
67/// Server status information
68#[derive(Debug, Serialize, Deserialize, Clone)]
69pub struct JiraServerStatus {
70    pub server_name: String,
71    pub version: String,
72    pub uptime_seconds: u64,
73    pub jira_url: String,
74    pub jira_connection_status: String,
75    pub authenticated_user: Option<String>,
76    pub cache_stats: cache::CacheStats,
77    pub tools_count: usize,
78}
79
80/// JIRA MCP Server
81///
82/// Main server implementation that provides AI-friendly tools for JIRA interaction.
83/// Uses the #[mcp_server] macro for automatic MCP infrastructure generation.
84#[mcp_server(
85    name = "JIRA MCP Server",
86    version = "0.7.0",
87    description = "AI-friendly JIRA integration server with semantic search, commenting, and relationship analysis capabilities",
88    auth = "disabled" // Start with disabled for development, can be changed to "file" for production
89)]
90#[derive(Clone)]
91pub struct JiraMcpServer {
92    /// Server start time for uptime calculation
93    start_time: Instant,
94
95    /// JIRA client for API operations
96    jira_client: Arc<JiraClient>,
97
98    /// Configuration
99    config: Arc<JiraConfig>,
100
101    /// Metadata cache
102    cache: Arc<MetadataCache>,
103
104    /// Tool implementations
105    search_tool: Arc<SearchIssuesTool>,
106    issue_details_tool: Arc<GetIssueDetailsTool>,
107    user_issues_tool: Arc<GetUserIssuesTool>,
108    list_attachments_tool: Arc<ListAttachmentsTool>,
109    download_attachment_tool: Arc<DownloadAttachmentTool>,
110    upload_attachment_tool: Arc<UploadAttachmentTool>,
111    add_comment_tool: Arc<AddCommentTool>,
112    issue_relationships_tool: Arc<IssueRelationshipsTool>,
113    update_description_tool: Arc<UpdateDescription>,
114    get_available_transitions_tool: Arc<GetAvailableTransitionsTool>,
115    transition_issue_tool: Arc<TransitionIssueTool>,
116    assign_issue_tool: Arc<AssignIssueTool>,
117    get_custom_fields_tool: Arc<GetCustomFieldsTool>,
118    update_custom_fields_tool: Arc<UpdateCustomFieldsTool>,
119    create_issue_tool: Arc<CreateIssueTool>,
120    get_create_metadata_tool: Arc<GetCreateMetadataTool>,
121    todo_tracker: Arc<TodoTracker>,
122    list_sprints_tool: Arc<ListSprintsTool>,
123    get_sprint_info_tool: Arc<GetSprintInfoTool>,
124    get_sprint_issues_tool: Arc<GetSprintIssuesTool>,
125    move_to_sprint_tool: Arc<MoveToSprintTool>,
126    create_sprint_tool: Arc<CreateSprintTool>,
127    start_sprint_tool: Arc<StartSprintTool>,
128    close_sprint_tool: Arc<CloseSprintTool>,
129    link_issues_tool: Arc<LinkIssuesTool>,
130    delete_issue_link_tool: Arc<DeleteIssueLinkTool>,
131    get_issue_link_types_tool: Arc<GetIssueLinkTypesTool>,
132    labels_tool: Arc<LabelsTool>,
133    components_tool: Arc<ComponentsTool>,
134    bulk_operations_tool: Arc<BulkOperationsTool>,
135}
136
137impl Default for JiraMcpServer {
138    fn default() -> Self {
139        // This is a placeholder default implementation
140        // In practice, the server should be created using `new()` or `with_config()`
141        panic!("JiraMcpServer cannot be created with default(). Use JiraMcpServer::new() instead.")
142    }
143}
144
145impl JiraMcpServer {
146    /// Create a new JIRA MCP Server with default configuration
147    #[instrument]
148    pub async fn new() -> JiraMcpResult<Self> {
149        info!("Initializing JIRA MCP Server");
150
151        // Load configuration
152        let config = Arc::new(JiraConfig::load()?);
153        info!("Configuration loaded successfully");
154
155        // Create cache
156        let cache = Arc::new(MetadataCache::new(config.cache_ttl_seconds));
157
158        // Start cache cleanup task
159        let _cleanup_handle = Arc::clone(&cache).start_cleanup_task();
160
161        // Create JIRA client
162        let jira_client = Arc::new(JiraClient::new(Arc::clone(&config)).await?);
163        info!("JIRA client initialized");
164
165        // Initialize current user in cache
166        if let Ok(current_user) = jira_client.get_current_user().await {
167            let user_mapping = UserMapping {
168                account_id: current_user.account_id,
169                display_name: current_user.display_name,
170                email_address: current_user.email_address,
171                username: None, // Will be filled if available
172            };
173
174            if let Err(e) = cache.set_current_user(user_mapping) {
175                warn!("Failed to cache current user: {}", e);
176            } else {
177                info!("Current user cached successfully");
178            }
179        } else {
180            warn!("Could not retrieve current user information");
181        }
182
183        // Create tool implementations
184        let search_tool = Arc::new(SearchIssuesTool::new(
185            Arc::clone(&jira_client),
186            Arc::clone(&config),
187            Arc::clone(&cache),
188        ));
189
190        let issue_details_tool = Arc::new(GetIssueDetailsTool::new(
191            Arc::clone(&jira_client),
192            Arc::clone(&config),
193            Arc::clone(&cache),
194        ));
195
196        let user_issues_tool = Arc::new(GetUserIssuesTool::new(
197            Arc::clone(&jira_client),
198            Arc::clone(&config),
199            Arc::clone(&cache),
200        ));
201
202        let list_attachments_tool = Arc::new(ListAttachmentsTool::new(
203            Arc::clone(&jira_client),
204            Arc::clone(&config),
205            Arc::clone(&cache),
206        ));
207
208        let download_attachment_tool = Arc::new(DownloadAttachmentTool::new(
209            Arc::clone(&jira_client),
210            Arc::clone(&config),
211            Arc::clone(&cache),
212        ));
213
214        let upload_attachment_tool = Arc::new(UploadAttachmentTool::new(
215            Arc::clone(&jira_client),
216            Arc::clone(&config),
217            Arc::clone(&cache),
218        ));
219
220        let add_comment_tool = Arc::new(AddCommentTool::new(
221            Arc::clone(&jira_client),
222            Arc::clone(&config),
223            Arc::clone(&cache),
224        ));
225
226        let issue_relationships_tool = Arc::new(IssueRelationshipsTool::new(
227            Arc::clone(&jira_client),
228            Arc::clone(&config),
229            Arc::clone(&cache),
230        ));
231
232        let update_description_tool = Arc::new(UpdateDescription::new(Arc::clone(&jira_client)));
233
234        let get_available_transitions_tool =
235            Arc::new(GetAvailableTransitionsTool::new(Arc::clone(&jira_client)));
236
237        let transition_issue_tool = Arc::new(TransitionIssueTool::new(Arc::clone(&jira_client)));
238
239        let assign_issue_tool = Arc::new(AssignIssueTool::new(Arc::clone(&jira_client)));
240
241        let get_custom_fields_tool = Arc::new(GetCustomFieldsTool::new(Arc::clone(&jira_client)));
242
243        let update_custom_fields_tool =
244            Arc::new(UpdateCustomFieldsTool::new(Arc::clone(&jira_client)));
245
246        let create_issue_tool = Arc::new(CreateIssueTool::new(Arc::clone(&jira_client)));
247
248        let get_create_metadata_tool =
249            Arc::new(GetCreateMetadataTool::new(Arc::clone(&jira_client)));
250
251        let todo_tracker = Arc::new(TodoTracker::new(
252            Arc::clone(&jira_client),
253            Arc::clone(&config),
254            Arc::clone(&cache),
255        ));
256
257        // Sprint management tools
258        let list_sprints_tool = Arc::new(ListSprintsTool::new(Arc::clone(&jira_client)));
259        let get_sprint_info_tool = Arc::new(GetSprintInfoTool::new(Arc::clone(&jira_client)));
260        let get_sprint_issues_tool = Arc::new(GetSprintIssuesTool::new(Arc::clone(&jira_client)));
261        let move_to_sprint_tool = Arc::new(MoveToSprintTool::new(Arc::clone(&jira_client)));
262        let create_sprint_tool = Arc::new(CreateSprintTool::new(Arc::clone(&jira_client)));
263        let start_sprint_tool = Arc::new(StartSprintTool::new(Arc::clone(&jira_client)));
264        let close_sprint_tool = Arc::new(CloseSprintTool::new(Arc::clone(&jira_client)));
265
266        // Issue linking tools
267        let link_issues_tool = Arc::new(LinkIssuesTool::new(Arc::clone(&jira_client)));
268        let delete_issue_link_tool = Arc::new(DeleteIssueLinkTool::new(Arc::clone(&jira_client)));
269        let get_issue_link_types_tool =
270            Arc::new(GetIssueLinkTypesTool::new(Arc::clone(&jira_client)));
271
272        // Labels and components tools
273        let labels_tool = Arc::new(LabelsTool::new(Arc::clone(&jira_client)));
274        let components_tool = Arc::new(ComponentsTool::new(Arc::clone(&jira_client)));
275
276        // Bulk operations tool
277        let bulk_operations_tool = Arc::new(BulkOperationsTool::new(Arc::clone(&jira_client)));
278
279        // Start auto-checkpoint background task (every 30 minutes)
280        let _auto_checkpoint_handle = Arc::clone(&todo_tracker).start_auto_checkpoint_task(30);
281        info!("Auto-checkpoint task started (interval: 30 minutes)");
282
283        info!("JIRA MCP Server initialized successfully");
284
285        Ok(Self {
286            start_time: Instant::now(),
287            jira_client,
288            config,
289            cache,
290            search_tool,
291            issue_details_tool,
292            user_issues_tool,
293            list_attachments_tool,
294            download_attachment_tool,
295            upload_attachment_tool,
296            add_comment_tool,
297            issue_relationships_tool,
298            update_description_tool,
299            get_available_transitions_tool,
300            transition_issue_tool,
301            assign_issue_tool,
302            get_custom_fields_tool,
303            update_custom_fields_tool,
304            create_issue_tool,
305            get_create_metadata_tool,
306            todo_tracker,
307            list_sprints_tool,
308            get_sprint_info_tool,
309            get_sprint_issues_tool,
310            move_to_sprint_tool,
311            create_sprint_tool,
312            start_sprint_tool,
313            close_sprint_tool,
314            link_issues_tool,
315            delete_issue_link_tool,
316            get_issue_link_types_tool,
317            labels_tool,
318            components_tool,
319            bulk_operations_tool,
320        })
321    }
322
323    /// Create server with custom configuration (for testing)
324    #[instrument(skip(config))]
325    pub async fn with_config(config: JiraConfig) -> JiraMcpResult<Self> {
326        let config = Arc::new(config);
327        let cache = Arc::new(MetadataCache::new(config.cache_ttl_seconds));
328        let _cleanup_handle = Arc::clone(&cache).start_cleanup_task();
329
330        let jira_client = Arc::new(JiraClient::new(Arc::clone(&config)).await?);
331
332        // Try to initialize current user
333        if let Ok(current_user) = jira_client.get_current_user().await {
334            let user_mapping = UserMapping {
335                account_id: current_user.account_id,
336                display_name: current_user.display_name,
337                email_address: current_user.email_address,
338                username: None,
339            };
340            let _ = cache.set_current_user(user_mapping);
341        }
342
343        let search_tool = Arc::new(SearchIssuesTool::new(
344            Arc::clone(&jira_client),
345            Arc::clone(&config),
346            Arc::clone(&cache),
347        ));
348
349        let issue_details_tool = Arc::new(GetIssueDetailsTool::new(
350            Arc::clone(&jira_client),
351            Arc::clone(&config),
352            Arc::clone(&cache),
353        ));
354
355        let user_issues_tool = Arc::new(GetUserIssuesTool::new(
356            Arc::clone(&jira_client),
357            Arc::clone(&config),
358            Arc::clone(&cache),
359        ));
360
361        let list_attachments_tool = Arc::new(ListAttachmentsTool::new(
362            Arc::clone(&jira_client),
363            Arc::clone(&config),
364            Arc::clone(&cache),
365        ));
366
367        let download_attachment_tool = Arc::new(DownloadAttachmentTool::new(
368            Arc::clone(&jira_client),
369            Arc::clone(&config),
370            Arc::clone(&cache),
371        ));
372
373        let upload_attachment_tool = Arc::new(UploadAttachmentTool::new(
374            Arc::clone(&jira_client),
375            Arc::clone(&config),
376            Arc::clone(&cache),
377        ));
378
379        let add_comment_tool = Arc::new(AddCommentTool::new(
380            Arc::clone(&jira_client),
381            Arc::clone(&config),
382            Arc::clone(&cache),
383        ));
384
385        let issue_relationships_tool = Arc::new(IssueRelationshipsTool::new(
386            Arc::clone(&jira_client),
387            Arc::clone(&config),
388            Arc::clone(&cache),
389        ));
390
391        let update_description_tool = Arc::new(UpdateDescription::new(Arc::clone(&jira_client)));
392
393        let get_available_transitions_tool =
394            Arc::new(GetAvailableTransitionsTool::new(Arc::clone(&jira_client)));
395
396        let transition_issue_tool = Arc::new(TransitionIssueTool::new(Arc::clone(&jira_client)));
397
398        let assign_issue_tool = Arc::new(AssignIssueTool::new(Arc::clone(&jira_client)));
399
400        let get_custom_fields_tool = Arc::new(GetCustomFieldsTool::new(Arc::clone(&jira_client)));
401
402        let update_custom_fields_tool =
403            Arc::new(UpdateCustomFieldsTool::new(Arc::clone(&jira_client)));
404
405        let create_issue_tool = Arc::new(CreateIssueTool::new(Arc::clone(&jira_client)));
406
407        let get_create_metadata_tool =
408            Arc::new(GetCreateMetadataTool::new(Arc::clone(&jira_client)));
409
410        let todo_tracker = Arc::new(TodoTracker::new(
411            Arc::clone(&jira_client),
412            Arc::clone(&config),
413            Arc::clone(&cache),
414        ));
415
416        // Sprint management tools
417        let list_sprints_tool = Arc::new(ListSprintsTool::new(Arc::clone(&jira_client)));
418        let get_sprint_info_tool = Arc::new(GetSprintInfoTool::new(Arc::clone(&jira_client)));
419        let get_sprint_issues_tool = Arc::new(GetSprintIssuesTool::new(Arc::clone(&jira_client)));
420        let move_to_sprint_tool = Arc::new(MoveToSprintTool::new(Arc::clone(&jira_client)));
421        let create_sprint_tool = Arc::new(CreateSprintTool::new(Arc::clone(&jira_client)));
422        let start_sprint_tool = Arc::new(StartSprintTool::new(Arc::clone(&jira_client)));
423        let close_sprint_tool = Arc::new(CloseSprintTool::new(Arc::clone(&jira_client)));
424
425        // Issue linking tools
426        let link_issues_tool = Arc::new(LinkIssuesTool::new(Arc::clone(&jira_client)));
427        let delete_issue_link_tool = Arc::new(DeleteIssueLinkTool::new(Arc::clone(&jira_client)));
428        let get_issue_link_types_tool =
429            Arc::new(GetIssueLinkTypesTool::new(Arc::clone(&jira_client)));
430
431        // Labels and components tools
432        let labels_tool = Arc::new(LabelsTool::new(Arc::clone(&jira_client)));
433        let components_tool = Arc::new(ComponentsTool::new(Arc::clone(&jira_client)));
434
435        // Bulk operations tool
436        let bulk_operations_tool = Arc::new(BulkOperationsTool::new(Arc::clone(&jira_client)));
437
438        Ok(Self {
439            start_time: Instant::now(),
440            jira_client,
441            config,
442            cache,
443            search_tool,
444            issue_details_tool,
445            user_issues_tool,
446            list_attachments_tool,
447            download_attachment_tool,
448            upload_attachment_tool,
449            add_comment_tool,
450            issue_relationships_tool,
451            update_description_tool,
452            get_available_transitions_tool,
453            transition_issue_tool,
454            assign_issue_tool,
455            get_custom_fields_tool,
456            update_custom_fields_tool,
457            create_issue_tool,
458            get_create_metadata_tool,
459            todo_tracker,
460            list_sprints_tool,
461            get_sprint_info_tool,
462            get_sprint_issues_tool,
463            move_to_sprint_tool,
464            create_sprint_tool,
465            start_sprint_tool,
466            close_sprint_tool,
467            link_issues_tool,
468            delete_issue_link_tool,
469            get_issue_link_types_tool,
470            labels_tool,
471            components_tool,
472            bulk_operations_tool,
473        })
474    }
475
476    /// Get server uptime in seconds
477    fn get_uptime_seconds(&self) -> u64 {
478        self.start_time.elapsed().as_secs()
479    }
480
481    /// Get current user display name (for status)
482    async fn get_current_user_name(&self) -> String {
483        if let Some(user) = self.cache.get_current_user() {
484            user.display_name
485        } else if let Ok(user) = self.jira_client.get_current_user().await {
486            user.display_name
487        } else {
488            "Unknown".to_string()
489        }
490    }
491}
492
493/// All public methods in this impl block become MCP tools automatically
494/// The #[mcp_tools] macro discovers these methods and exposes them via MCP
495#[mcp_tools]
496impl JiraMcpServer {
497    /// Search for JIRA issues using AI-friendly semantic parameters
498    ///
499    /// This tool allows AI agents to search for issues without needing to know JQL syntax.
500    /// It accepts natural language parameters and translates them to appropriate JIRA queries.
501    ///
502    /// # Examples
503    /// - Find all stories assigned to me: `{"issue_types": ["story"], "assigned_to": "me"}`
504    /// - Find bugs in project FOO: `{"issue_types": ["bug"], "project_key": "FOO"}`
505    /// - Find overdue issues: `{"status": ["open"], "created_after": "30 days ago"}`
506    #[instrument(skip(self))]
507    pub async fn search_issues(
508        &self,
509        params: SearchIssuesParams,
510    ) -> anyhow::Result<SearchIssuesResult> {
511        self.search_tool.execute(params).await.map_err(|e| {
512            error!("search_issues failed: {}", e);
513            anyhow::anyhow!(e)
514        })
515    }
516
517    /// Get detailed information about a specific JIRA issue
518    ///
519    /// Retrieves comprehensive information about an issue including summary, description,
520    /// status, assignee, and optionally comments, attachments, and history.
521    ///
522    /// # Examples
523    /// - Get basic issue info: `{"issue_key": "PROJ-123"}`
524    /// - Get issue with comments: `{"issue_key": "PROJ-123", "include_comments": true}`
525    /// - Get full issue details: `{"issue_key": "PROJ-123", "include_comments": true, "include_attachments": true, "include_history": true}`
526    #[instrument(skip(self))]
527    pub async fn get_issue_details(
528        &self,
529        params: GetIssueDetailsParams,
530    ) -> anyhow::Result<GetIssueDetailsResult> {
531        self.issue_details_tool.execute(params).await.map_err(|e| {
532            error!("get_issue_details failed: {}", e);
533            anyhow::anyhow!(e)
534        })
535    }
536
537    /// Get issues assigned to a specific user with filtering options
538    ///
539    /// Retrieves issues assigned to a user (defaults to current user) with various
540    /// semantic filtering options for status, type, project, priority, and dates.
541    ///
542    /// # Examples
543    /// - Get my open issues: `{"status_filter": ["open", "in_progress"]}`
544    /// - Get user's bugs: `{"username": "john.doe", "issue_types": ["bug"]}`
545    /// - Get overdue issues: `{"due_date_filter": "overdue", "priority_filter": ["high"]}`
546    #[instrument(skip(self))]
547    pub async fn get_user_issues(
548        &self,
549        params: GetUserIssuesParams,
550    ) -> anyhow::Result<GetUserIssuesResult> {
551        self.user_issues_tool.execute(params).await.map_err(|e| {
552            error!("get_user_issues failed: {}", e);
553            anyhow::anyhow!(e)
554        })
555    }
556
557    /// Get server status and connection information
558    ///
559    /// Returns comprehensive information about the server status, JIRA connection,
560    /// authenticated user, cache statistics, and available tools.
561    #[instrument(skip(self))]
562    pub async fn get_server_status(&self) -> anyhow::Result<JiraServerStatus> {
563        info!("Getting server status");
564
565        let connection_status = match self.jira_client.get_current_user().await {
566            Ok(_) => "Connected".to_string(),
567            Err(e) => format!("Connection Error: {}", e),
568        };
569
570        let authenticated_user = if connection_status == "Connected" {
571            Some(self.get_current_user_name().await)
572        } else {
573            None
574        };
575
576        Ok(JiraServerStatus {
577            server_name: "JIRA MCP Server".to_string(),
578            version: "0.8.0".to_string(),
579            uptime_seconds: self.get_uptime_seconds(),
580            jira_url: self.config.jira_url.clone(),
581            jira_connection_status: connection_status,
582            authenticated_user,
583            cache_stats: self.cache.get_stats(),
584            tools_count: 48, // search_issues, get_issue_details, get_user_issues, list_issue_attachments, download_attachment, upload_attachment, get_server_status, clear_cache, test_connection, add_comment, update_issue_description, get_issue_relationships, get_available_transitions, transition_issue, assign_issue, get_custom_fields, update_custom_fields, create_issue, get_create_metadata, list_todos, add_todo, update_todo, start_todo_work, complete_todo_work, checkpoint_todo_work, pause_todo_work, cancel_todo_work, get_active_work_sessions, set_todo_base, list_sprints, get_sprint_info, get_sprint_issues, move_to_sprint, create_sprint, start_sprint, close_sprint, link_issues, delete_issue_link, get_issue_link_types, manage_labels, get_available_labels, update_components, get_available_components, bulk_create_issues, bulk_transition_issues, bulk_update_fields, bulk_assign_issues, bulk_add_labels
585        })
586    }
587
588    /// Clear all cached metadata
589    ///
590    /// Clears all cached metadata including board mappings, project info, user info,
591    /// and issue types. Useful when JIRA configuration changes or for troubleshooting.
592    #[instrument(skip(self))]
593    pub async fn clear_cache(&self) -> anyhow::Result<String> {
594        info!("Clearing all cached metadata");
595
596        match self.cache.clear_all() {
597            Ok(()) => {
598                info!("Cache cleared successfully");
599                Ok("All cached metadata has been cleared successfully".to_string())
600            }
601            Err(e) => {
602                error!("Failed to clear cache: {}", e);
603                Err(anyhow::anyhow!("Failed to clear cache: {}", e))
604            }
605        }
606    }
607
608    /// List all attachments for a specific JIRA issue
609    ///
610    /// Returns metadata about all attachments on an issue, including filenames,
611    /// sizes, content types, and attachment IDs needed for downloading.
612    ///
613    /// # Examples
614    /// - List all attachments: `{"issue_key": "PROJ-123"}`
615    #[instrument(skip(self))]
616    pub async fn list_issue_attachments(
617        &self,
618        params: ListAttachmentsParams,
619    ) -> anyhow::Result<ListAttachmentsResult> {
620        self.list_attachments_tool
621            .execute(params)
622            .await
623            .map_err(|e| {
624                error!("list_issue_attachments failed: {}", e);
625                anyhow::anyhow!(e)
626            })
627    }
628
629    /// Download attachment content from a JIRA issue
630    ///
631    /// Downloads the actual content of an attachment given its attachment ID.
632    /// Content is returned as base64 encoded string by default for safety.
633    ///
634    /// # Examples
635    /// - Download attachment: `{"attachment_id": "12345"}`
636    /// - Download with size limit: `{"attachment_id": "12345", "max_size_bytes": 5242880}`
637    /// - Download as raw content: `{"attachment_id": "12345", "base64_encoded": false}`
638    pub async fn download_attachment(
639        &self,
640        params: DownloadAttachmentParams,
641    ) -> anyhow::Result<DownloadAttachmentResult> {
642        self.download_attachment_tool
643            .execute(params)
644            .await
645            .map_err(|e| {
646                error!("download_attachment failed: {}", e);
647                anyhow::anyhow!(e)
648            })
649    }
650
651    /// Upload attachments to a JIRA issue
652    ///
653    /// Adds one or more files as attachments to an existing JIRA issue.
654    /// Supports both inline base64 content and reading from filesystem.
655    ///
656    /// # Features
657    /// - Upload multiple files in a single operation
658    /// - Inline base64 content OR filesystem paths
659    /// - Automatic MIME type detection
660    /// - Size limits for safety (default 10MB total)
661    /// - Secure path validation for filesystem access
662    ///
663    /// # Examples
664    /// - Upload from inline content: `{"issue_key": "PROJ-123", "files": [{"filename": "doc.pdf", "content_base64": "..."}]}`
665    /// - Upload from filesystem: `{"issue_key": "PROJ-123", "file_paths": ["reports/report.pdf"]}`
666    pub async fn upload_attachment(
667        &self,
668        params: UploadAttachmentParams,
669    ) -> anyhow::Result<UploadAttachmentResult> {
670        self.upload_attachment_tool
671            .execute(params)
672            .await
673            .map_err(|e| {
674                error!("upload_attachment failed: {}", e);
675                anyhow::anyhow!(e)
676            })
677    }
678
679    /// Test JIRA connection and authentication
680    ///
681    /// Performs a connection test to the configured JIRA instance and returns
682    /// detailed information about the connection status and authenticated user.
683    #[instrument(skip(self))]
684    pub async fn test_connection(&self) -> anyhow::Result<String> {
685        info!("Testing JIRA connection");
686
687        match self.jira_client.get_current_user().await {
688            Ok(user) => {
689                let message = format!(
690                    "✅ Connection successful!\n\
691                     JIRA URL: {}\n\
692                     Authenticated as: {} ({})\n\
693                     Account ID: {}\n\
694                     Email: {}",
695                    self.config.jira_url,
696                    user.display_name,
697                    user.email_address.as_deref().unwrap_or("N/A"),
698                    user.account_id,
699                    user.email_address.as_deref().unwrap_or("Not provided")
700                );
701                info!("Connection test successful for user: {}", user.display_name);
702                Ok(message)
703            }
704            Err(e) => {
705                let message = format!(
706                    "❌ Connection failed!\n\
707                     JIRA URL: {}\n\
708                     Error: {}\n\
709                     \n\
710                     Please check:\n\
711                     - JIRA URL is correct and accessible\n\
712                     - Authentication credentials are valid\n\
713                     - Network connectivity to JIRA instance",
714                    self.config.jira_url, e
715                );
716                error!("Connection test failed: {}", e);
717                Ok(message) // Return as success with error message for user feedback
718            }
719        }
720    }
721
722    /// Add a comment to a JIRA issue
723    ///
724    /// Adds a comment to the specified JIRA issue with the provided text content.
725    /// This tool provides a simple way to add comments without requiring knowledge
726    /// of JIRA's comment API structure.
727    ///
728    /// # Examples
729    /// - Add a simple comment: `{"issue_key": "PROJ-123", "comment_body": "This looks good to me!"}`
730    /// - Add a detailed comment: `{"issue_key": "PROJ-123", "comment_body": "I've tested this feature and found the following:\n\n1. Works as expected\n2. Performance is good\n3. Ready for deployment"}`
731    #[instrument(skip(self))]
732    pub async fn add_comment(&self, params: AddCommentParams) -> anyhow::Result<AddCommentResult> {
733        self.add_comment_tool.execute(params).await.map_err(|e| {
734            error!("add_comment failed: {}", e);
735            anyhow::anyhow!(e)
736        })
737    }
738
739    /// Update the description of a JIRA issue
740    ///
741    /// Updates the description field of a JIRA issue. Supports three modes:
742    /// - append (default): Adds content to the end of the existing description
743    /// - prepend: Adds content to the beginning of the existing description
744    /// - replace: Completely replaces the description with new content
745    ///
746    /// # Examples
747    /// - Append to description: `{"issue_key": "PROJ-123", "content": "Additional context: This fixes the login issue"}`
748    /// - Replace description: `{"issue_key": "PROJ-123", "content": "New complete description", "mode": "replace"}`
749    /// - Prepend to description: `{"issue_key": "PROJ-123", "content": "⚠️ URGENT: ", "mode": "prepend"}`
750    #[instrument(skip(self))]
751    pub async fn update_issue_description(
752        &self,
753        params: UpdateDescriptionParams,
754    ) -> anyhow::Result<UpdateDescriptionResult> {
755        self.update_description_tool
756            .execute(params)
757            .await
758            .map_err(|e| {
759                error!("update_issue_description failed: {}", e);
760                anyhow::anyhow!(e)
761            })
762    }
763
764    /// Extract issue relationship graph
765    ///
766    /// Analyzes JIRA issue relationships to build a comprehensive relationship graph
767    /// showing how issues are connected through links, subtasks, epics, and other relationships.
768    /// This tool helps understand issue dependencies, blockers, and project structure.
769    ///
770    /// # Examples
771    /// - Basic relationship extraction: `{"root_issue_key": "PROJ-123"}`
772    /// - Deep relationship analysis: `{"root_issue_key": "PROJ-123", "max_depth": 3}`
773    /// - Custom relationship filters: `{"root_issue_key": "PROJ-123", "include_duplicates": true, "include_epic_links": false}`
774    #[instrument(skip(self))]
775    pub async fn get_issue_relationships(
776        &self,
777        params: IssueRelationshipsParams,
778    ) -> anyhow::Result<IssueRelationshipsResult> {
779        self.issue_relationships_tool
780            .execute(params)
781            .await
782            .map_err(|e| {
783                error!("get_issue_relationships failed: {}", e);
784                anyhow::anyhow!(e)
785            })
786    }
787
788    /// Get available transitions for an issue
789    ///
790    /// Returns the list of workflow transitions available for a specific JIRA issue.
791    /// Different issues may have different available transitions depending on their
792    /// current status, workflow, and issue type.
793    ///
794    /// # Examples
795    /// - Get available transitions: `{"issue_key": "PROJ-123"}`
796    #[instrument(skip(self))]
797    pub async fn get_available_transitions(
798        &self,
799        params: GetAvailableTransitionsParams,
800    ) -> anyhow::Result<GetAvailableTransitionsResult> {
801        self.get_available_transitions_tool
802            .execute(params)
803            .await
804            .map_err(|e| {
805                error!("get_available_transitions failed: {}", e);
806                anyhow::anyhow!(e)
807            })
808    }
809
810    /// Transition an issue to a new status
811    ///
812    /// Executes a workflow transition on a JIRA issue to change its status.
813    /// You can specify the transition either by ID or by name. Optionally add
814    /// a comment and/or set a resolution when transitioning.
815    ///
816    /// # Examples
817    /// - Transition by name: `{"issue_key": "PROJ-123", "transition_name": "Start Progress"}`
818    /// - Transition by ID: `{"issue_key": "PROJ-123", "transition_id": "11"}`
819    /// - Transition with comment: `{"issue_key": "PROJ-123", "transition_name": "Done", "comment": "Work completed"}`
820    /// - Transition with resolution: `{"issue_key": "PROJ-123", "transition_name": "Done", "resolution": "Fixed"}`
821    #[instrument(skip(self))]
822    pub async fn transition_issue(
823        &self,
824        params: TransitionIssueParams,
825    ) -> anyhow::Result<TransitionIssueResult> {
826        self.transition_issue_tool
827            .execute(params)
828            .await
829            .map_err(|e| {
830                error!("transition_issue failed: {}", e);
831                anyhow::anyhow!(e)
832            })
833    }
834
835    /// Assign a JIRA issue to a user
836    ///
837    /// Assigns an issue to a specific user or unassigns it. You can use:
838    /// - "me" or "self" to assign to yourself
839    /// - A specific username or account ID
840    /// - null/empty to unassign the issue
841    ///
842    /// This is particularly useful for:
843    /// - Automated testing (assign issues to yourself)
844    /// - Workflow automation (assign based on conditions)
845    /// - Task distribution (assign to team members)
846    ///
847    /// # Examples
848    /// - Assign to yourself: `{"issue_key": "PROJ-123", "assignee": "me"}`
849    /// - Assign to user: `{"issue_key": "PROJ-123", "assignee": "john.doe@example.com"}`
850    /// - Unassign: `{"issue_key": "PROJ-123", "assignee": null}`
851    #[instrument(skip(self))]
852    pub async fn assign_issue(
853        &self,
854        params: AssignIssueParams,
855    ) -> anyhow::Result<AssignIssueResult> {
856        self.assign_issue_tool.execute(params).await.map_err(|e| {
857            error!("assign_issue failed: {}", e);
858            anyhow::anyhow!(e)
859        })
860    }
861
862    /// Get custom fields from a JIRA issue
863    ///
864    /// Discovers and returns all custom fields present in a JIRA issue, including
865    /// their field IDs, types, current values, and human-readable displays.
866    /// Also attempts to detect common fields like story points and acceptance criteria.
867    ///
868    /// This is useful for:
869    /// - Understanding what custom fields are available in your JIRA instance
870    /// - Finding the correct field ID for updating custom fields
871    /// - Inspecting current custom field values
872    ///
873    /// # Examples
874    /// - Get all custom fields: `{"issue_key": "PROJ-123"}`
875    #[instrument(skip(self))]
876    pub async fn get_custom_fields(
877        &self,
878        params: GetCustomFieldsParams,
879    ) -> anyhow::Result<GetCustomFieldsResult> {
880        self.get_custom_fields_tool
881            .execute(params)
882            .await
883            .map_err(|e| {
884                error!("get_custom_fields failed: {}", e);
885                anyhow::anyhow!(e)
886            })
887    }
888
889    /// Update custom fields in a JIRA issue
890    ///
891    /// Updates custom field values in a JIRA issue. Supports updating by field ID
892    /// or using convenience parameters for common fields like story points and
893    /// acceptance criteria (with automatic field detection).
894    ///
895    /// This tool allows you to:
896    /// - Update story points using auto-detection or explicit field ID
897    /// - Update acceptance criteria using auto-detection or explicit field ID
898    /// - Update any custom field by providing its field ID and value
899    ///
900    /// # Examples
901    /// - Set story points: `{"issue_key": "PROJ-123", "story_points": 5}`
902    /// - Set acceptance criteria: `{"issue_key": "PROJ-123", "acceptance_criteria": "User can login successfully"}`
903    /// - Update specific field: `{"issue_key": "PROJ-123", "custom_field_updates": {"customfield_10050": "value"}}`
904    /// - Override field ID: `{"issue_key": "PROJ-123", "story_points": 8, "story_points_field_id": "customfield_10016"}`
905    #[instrument(skip(self))]
906    pub async fn update_custom_fields(
907        &self,
908        params: UpdateCustomFieldsParams,
909    ) -> anyhow::Result<UpdateCustomFieldsResult> {
910        self.update_custom_fields_tool
911            .execute(params)
912            .await
913            .map_err(|e| {
914                error!("update_custom_fields failed: {}", e);
915                anyhow::anyhow!(e)
916            })
917    }
918
919    /// Get issue creation metadata for a JIRA project
920    ///
921    /// Discovers what issue types are available in a project and what fields are
922    /// required or optional for each type. This is essential for understanding what
923    /// parameters to provide when creating issues, especially for custom fields.
924    ///
925    /// Use this tool to:
926    /// - Discover available issue types (Task, Bug, Story, Epic, etc.)
927    /// - Find required fields for a specific issue type
928    /// - Get allowed values for constrained fields (priorities, components, etc.)
929    /// - Identify custom field IDs and their types
930    ///
931    /// # Examples
932    /// - Get all issue types: `{"project_key": "PROJ"}`
933    /// - Get Bug metadata only: `{"project_key": "PROJ", "issue_type": "Bug"}`
934    /// - Get detailed schemas: `{"project_key": "PROJ", "issue_type": "Story", "include_schemas": true}`
935    #[instrument(skip(self))]
936    pub async fn get_create_metadata(
937        &self,
938        params: GetCreateMetadataParams,
939    ) -> anyhow::Result<GetCreateMetadataResult> {
940        self.get_create_metadata_tool
941            .execute(params)
942            .await
943            .map_err(|e| {
944                error!("get_create_metadata failed: {}", e);
945                anyhow::anyhow!(e)
946            })
947    }
948
949    /// Create a new JIRA issue
950    ///
951    /// Creates a new issue in JIRA with comprehensive parameter support.
952    /// Designed to be simple for basic use cases while supporting advanced features.
953    ///
954    /// Key features:
955    /// - Simple: Just provide summary and project_key for basic tasks
956    /// - Smart defaults: Auto-detects subtasks, handles "assign_to_me", etc.
957    /// - initial_todos: Automatically formats todo checklists
958    /// - Custom fields: Full support for any custom field
959    /// - Epic/Story points: Convenience parameters with auto-detection
960    ///
961    /// IMPORTANT: Use get_create_metadata first to discover:
962    /// - Available issue types for your project
963    /// - Required fields for each issue type
964    /// - Allowed values for constrained fields
965    /// - Custom field IDs and types
966    ///
967    /// # Examples
968    /// - Simple task: `{"project_key": "PROJ", "summary": "Fix login bug"}`
969    /// - Bug with priority: `{"project_key": "PROJ", "summary": "Payment fails", "issue_type": "Bug", "priority": "High"}`
970    /// - Story with todos: `{"project_key": "PROJ", "summary": "Dark mode", "issue_type": "Story", "initial_todos": ["Design colors", "Implement toggle"], "assign_to_me": true}`
971    /// - Subtask: `{"parent_issue_key": "PROJ-123", "summary": "Write tests"}`
972    #[instrument(skip(self))]
973    pub async fn create_issue(
974        &self,
975        params: CreateIssueParams,
976    ) -> anyhow::Result<CreateIssueResult> {
977        self.create_issue_tool.execute(params).await.map_err(|e| {
978            error!("create_issue failed: {}", e);
979            anyhow::anyhow!(e)
980        })
981    }
982
983    /// List todos from an issue description
984    ///
985    /// Parses markdown-style checkboxes from an issue description and returns
986    /// them as structured todo items. Supports formats like `- [ ] todo` and `- [x] completed`.
987    /// Allows filtering by status: open, completed, or wip (work in progress).
988    ///
989    /// # Examples
990    /// - List all todos: `{"issue_key": "PROJ-123"}`
991    /// - List open todos: `{"status_filter": ["open"]}`
992    /// - List work in progress: `{"status_filter": ["wip"]}`
993    /// - List open and wip: `{"status_filter": ["open", "wip"]}`
994    #[instrument(skip(self))]
995    pub async fn list_todos(&self, params: ListTodosParams) -> anyhow::Result<ListTodosResult> {
996        self.todo_tracker.list_todos(params).await.map_err(|e| {
997            error!("list_todos failed: {}", e);
998            anyhow::anyhow!(e)
999        })
1000    }
1001
1002    /// Add a new todo to an issue description
1003    ///
1004    /// Adds a new markdown-style checkbox todo to an issue's description.
1005    /// Automatically creates a "Todos" section if one doesn't exist, or adds
1006    /// to an existing todo section.
1007    ///
1008    /// # Examples
1009    /// - Add todo at end: `{"issue_key": "PROJ-123", "todo_text": "Review code changes"}`
1010    /// - Add todo at beginning: `{"issue_key": "PROJ-123", "todo_text": "Urgent: Fix bug", "prepend": true}`
1011    #[instrument(skip(self))]
1012    pub async fn add_todo(&self, params: AddTodoParams) -> anyhow::Result<AddTodoResult> {
1013        self.todo_tracker.add_todo(params).await.map_err(|e| {
1014            error!("add_todo failed: {}", e);
1015            anyhow::anyhow!(e)
1016        })
1017    }
1018
1019    /// Update a todo's completion status
1020    ///
1021    /// Marks a todo as completed (checked) or incomplete (unchecked) in the issue description.
1022    /// You can specify the todo by its ID or by its 1-based index in the list.
1023    ///
1024    /// # Examples
1025    /// - Complete a todo: `{"issue_key": "PROJ-123", "todo_id_or_index": "1", "completed": true}`
1026    /// - Reopen a todo: `{"issue_key": "PROJ-123", "todo_id_or_index": "todo-abc123", "completed": false}`
1027    #[instrument(skip(self))]
1028    pub async fn update_todo(&self, params: UpdateTodoParams) -> anyhow::Result<UpdateTodoResult> {
1029        self.todo_tracker.update_todo(params).await.map_err(|e| {
1030            error!("update_todo failed: {}", e);
1031            anyhow::anyhow!(e)
1032        })
1033    }
1034
1035    /// Start tracking work time on a todo
1036    ///
1037    /// Begins tracking time spent working on a specific todo. Creates a work session
1038    /// that will be used to calculate time when you complete the work. You must
1039    /// complete the work session before starting another one on the same todo.
1040    ///
1041    /// IMPORTANT: When you complete, pause, or checkpoint work (which logs time to JIRA),
1042    /// the issue MUST have an "Original Estimate" or "Remaining Estimate" field set.
1043    /// If the issue doesn't have an estimate, you'll get a clear error with instructions.
1044    ///
1045    /// # Examples
1046    /// - Start work on first todo: `{"issue_key": "PROJ-123", "todo_id_or_index": "1"}`
1047    /// - Start work by todo ID: `{"issue_key": "PROJ-123", "todo_id_or_index": "todo-abc123"}`
1048    #[instrument(skip(self))]
1049    pub async fn start_todo_work(
1050        &self,
1051        params: StartTodoWorkParams,
1052    ) -> anyhow::Result<StartTodoWorkResult> {
1053        self.todo_tracker
1054            .start_todo_work(params)
1055            .await
1056            .map_err(|e| {
1057                error!("start_todo_work failed: {}", e);
1058                anyhow::anyhow!(e)
1059            })
1060    }
1061
1062    /// Complete work on a todo and log time spent
1063    ///
1064    /// Completes a work session for a todo, calculates the time spent, and logs it
1065    /// as a worklog entry in JIRA. Optionally marks the todo as completed.
1066    ///
1067    /// IMPORTANT REQUIREMENTS:
1068    /// 1. The issue MUST have an "Original Estimate" or "Remaining Estimate" field set in JIRA before logging time.
1069    ///    If not set, you'll receive a clear error with instructions on how to fix it.
1070    /// 2. For sessions spanning multiple days (>24 hours), you MUST provide explicit time using
1071    ///    time_spent_hours, time_spent_minutes, or time_spent_seconds to prevent logging extremely long sessions.
1072    ///
1073    /// # Examples
1074    /// - Complete same-day work: `{"todo_id_or_index": "1"}`
1075    /// - Multi-day work: `{"todo_id_or_index": "1", "time_spent_hours": 8.5}`
1076    /// - With minutes: `{"todo_id_or_index": "1", "time_spent_minutes": 480}`
1077    /// - Without marking done: `{"todo_id_or_index": "1", "time_spent_hours": 6, "mark_completed": false}`
1078    /// - With comment: `{"todo_id_or_index": "1", "time_spent_hours": 7, "worklog_comment": "Completed feature implementation"}`
1079    #[instrument(skip(self))]
1080    pub async fn complete_todo_work(
1081        &self,
1082        params: CompleteTodoWorkParams,
1083    ) -> anyhow::Result<CompleteTodoWorkResult> {
1084        self.todo_tracker
1085            .complete_todo_work(params)
1086            .await
1087            .map_err(|e| {
1088                error!("complete_todo_work failed: {}", e);
1089                anyhow::anyhow!(e)
1090            })
1091    }
1092
1093    /// Checkpoint work progress - log time but keep session active
1094    ///
1095    /// Creates a checkpoint by logging the time accumulated since the session started
1096    /// (or since the last checkpoint), then resets the timer to continue tracking.
1097    /// Perfect for logging progress during long work sessions without stopping the timer.
1098    ///
1099    /// IMPORTANT: The issue MUST have an "Original Estimate" or "Remaining Estimate" field
1100    /// set in JIRA before checkpointing. If not set, you'll receive a clear error message
1101    /// with instructions. Ask the user to set an estimate in JIRA first.
1102    ///
1103    /// Benefits:
1104    /// - Avoid multi-day session issues by checkpointing before midnight
1105    /// - Create incremental progress records in JIRA
1106    /// - Maintain accurate time tracking for long sessions
1107    /// - Survive server restarts with logged time
1108    ///
1109    /// # Examples
1110    /// - Regular checkpoint: `{"todo_id_or_index": "1"}`
1111    /// - With comment: `{"todo_id_or_index": "1", "worklog_comment": "Completed initial implementation"}`
1112    /// - End of day checkpoint: `{"todo_id_or_index": "1", "worklog_comment": "End of day checkpoint, will continue tomorrow"}`
1113    #[instrument(skip(self))]
1114    pub async fn checkpoint_todo_work(
1115        &self,
1116        params: CheckpointTodoWorkParams,
1117    ) -> anyhow::Result<CheckpointTodoWorkResult> {
1118        self.todo_tracker
1119            .checkpoint_todo_work(params)
1120            .await
1121            .map_err(|e| {
1122                error!("checkpoint_todo_work failed: {}", e);
1123                anyhow::anyhow!(e)
1124            })
1125    }
1126
1127    /// Set the base issue for todo operations
1128    ///
1129    /// Sets a default JIRA issue to use for all subsequent todo commands.
1130    /// After setting a base issue, you can omit the issue_key parameter in
1131    /// list_todos, add_todo, update_todo, start_todo_work, and complete_todo_work.
1132    ///
1133    /// # Examples
1134    /// - Set base issue: `{"issue_key": "PROJ-123"}`
1135    ///
1136    /// Then you can use:
1137    /// - `list_todos({})` instead of `list_todos({"issue_key": "PROJ-123"})`
1138    /// - `add_todo({"todo_text": "New task"})` instead of providing issue_key
1139    #[instrument(skip(self))]
1140    pub async fn set_todo_base(
1141        &self,
1142        params: SetTodoBaseParams,
1143    ) -> anyhow::Result<SetTodoBaseResult> {
1144        self.todo_tracker.set_todo_base(params).await.map_err(|e| {
1145            error!("set_todo_base failed: {}", e);
1146            anyhow::anyhow!(e)
1147        })
1148    }
1149
1150    /// Pause work on a todo and save progress
1151    ///
1152    /// Stops the active work session, calculates time spent, and logs it to JIRA.
1153    /// Unlike complete_todo_work, this doesn't mark the todo as completed - perfect
1154    /// for end-of-day saves or when you need to switch tasks temporarily.
1155    ///
1156    /// IMPORTANT: The issue MUST have an "Original Estimate" or "Remaining Estimate" field
1157    /// set in JIRA before pausing. If not set, you'll receive a clear error message with
1158    /// instructions. Ask the user to set an estimate in JIRA first.
1159    ///
1160    /// # Examples
1161    /// - Pause at end of day: `{"todo_id_or_index": "1", "worklog_comment": "End of day, will continue tomorrow"}`
1162    /// - Quick pause: `{"todo_id_or_index": "1"}`
1163    #[instrument(skip(self))]
1164    pub async fn pause_todo_work(
1165        &self,
1166        params: PauseTodoWorkParams,
1167    ) -> anyhow::Result<PauseTodoWorkResult> {
1168        self.todo_tracker
1169            .pause_todo_work(params)
1170            .await
1171            .map_err(|e| {
1172                error!("pause_todo_work failed: {}", e);
1173                anyhow::anyhow!(e)
1174            })
1175    }
1176
1177    /// Cancel an active work session without logging time
1178    ///
1179    /// Discards the current work session without creating a worklog entry.
1180    /// Useful when you started tracking the wrong todo or need to abandon work.
1181    ///
1182    /// # Examples
1183    /// - Cancel wrong session: `{"todo_id_or_index": "1"}`
1184    #[instrument(skip(self))]
1185    pub async fn cancel_todo_work(
1186        &self,
1187        params: CancelTodoWorkParams,
1188    ) -> anyhow::Result<CancelTodoWorkResult> {
1189        self.todo_tracker
1190            .cancel_todo_work(params)
1191            .await
1192            .map_err(|e| {
1193                error!("cancel_todo_work failed: {}", e);
1194                anyhow::anyhow!(e)
1195            })
1196    }
1197
1198    /// Get all active work sessions
1199    ///
1200    /// Returns a list of all currently active work sessions showing what's being
1201    /// tracked, when it started, and how long you've been working on it.
1202    ///
1203    /// # Examples
1204    /// - List all active sessions: `{}`
1205    #[instrument(skip(self))]
1206    pub async fn get_active_work_sessions(&self) -> anyhow::Result<GetActiveWorkSessionsResult> {
1207        self.todo_tracker
1208            .get_active_work_sessions()
1209            .await
1210            .map_err(|e| {
1211                error!("get_active_work_sessions failed: {}", e);
1212                anyhow::anyhow!(e)
1213            })
1214    }
1215
1216    /// List sprints for a specific board
1217    ///
1218    /// Returns all sprints for a board, with optional filtering by state (active, future, closed).
1219    /// Supports pagination for boards with many sprints.
1220    ///
1221    /// # Examples
1222    /// - List all sprints for a board: `{"board_id": 1}`
1223    /// - List only active sprints: `{"board_id": 1, "state": "active"}`
1224    /// - List with pagination: `{"board_id": 1, "limit": 20, "start_at": 0}`
1225    #[instrument(skip(self))]
1226    pub async fn list_sprints(
1227        &self,
1228        params: ListSprintsParams,
1229    ) -> anyhow::Result<ListSprintsResult> {
1230        self.list_sprints_tool
1231            .execute(params)
1232            .await
1233            .map_err(|e: JiraMcpError| {
1234                error!("list_sprints failed: {}", e);
1235                anyhow::anyhow!(e)
1236            })
1237    }
1238
1239    /// Get detailed information about a specific sprint
1240    ///
1241    /// Retrieves sprint details including name, state, start/end dates, and board information.
1242    ///
1243    /// # Examples
1244    /// - Get sprint info: `{"sprint_id": 123}`
1245    #[instrument(skip(self))]
1246    pub async fn get_sprint_info(
1247        &self,
1248        params: GetSprintInfoParams,
1249    ) -> anyhow::Result<GetSprintInfoResult> {
1250        self.get_sprint_info_tool
1251            .execute(params)
1252            .await
1253            .map_err(|e: JiraMcpError| {
1254                error!("get_sprint_info failed: {}", e);
1255                anyhow::anyhow!(e)
1256            })
1257    }
1258
1259    /// Get all issues in a specific sprint
1260    ///
1261    /// Returns all issues that are currently in the sprint, with pagination support.
1262    ///
1263    /// # Examples
1264    /// - Get all sprint issues: `{"sprint_id": 123}`
1265    /// - Get with pagination: `{"sprint_id": 123, "limit": 50, "start_at": 0}`
1266    #[instrument(skip(self))]
1267    pub async fn get_sprint_issues(
1268        &self,
1269        params: GetSprintIssuesParams,
1270    ) -> anyhow::Result<GetSprintIssuesResult> {
1271        self.get_sprint_issues_tool
1272            .execute(params)
1273            .await
1274            .map_err(|e: JiraMcpError| {
1275                error!("get_sprint_issues failed: {}", e);
1276                anyhow::anyhow!(e)
1277            })
1278    }
1279
1280    /// Move issues to a sprint
1281    ///
1282    /// Moves one or more issues to the specified sprint. Issues must exist and be accessible.
1283    ///
1284    /// # Examples
1285    /// - Move single issue: `{"sprint_id": 123, "issue_keys": ["PROJ-456"]}`
1286    /// - Move multiple issues: `{"sprint_id": 123, "issue_keys": ["PROJ-456", "PROJ-789"]}`
1287    #[instrument(skip(self))]
1288    pub async fn move_to_sprint(
1289        &self,
1290        params: MoveToSprintParams,
1291    ) -> anyhow::Result<MoveToSprintResult> {
1292        self.move_to_sprint_tool
1293            .execute(params)
1294            .await
1295            .map_err(|e: JiraMcpError| {
1296                error!("move_to_sprint failed: {}", e);
1297                anyhow::anyhow!(e)
1298            })
1299    }
1300
1301    /// Create a new sprint on a board
1302    ///
1303    /// Creates a future sprint with the specified name. Dates can be set immediately
1304    /// or later when starting the sprint.
1305    ///
1306    /// # Examples
1307    /// - Create basic sprint: `{"board_id": 1, "name": "Sprint 42"}`
1308    /// - With dates: `{"board_id": 1, "name": "Sprint 42", "start_date": "2025-01-20T00:00:00Z", "end_date": "2025-02-03T23:59:59Z"}`
1309    #[instrument(skip(self))]
1310    pub async fn create_sprint(
1311        &self,
1312        params: CreateSprintParams,
1313    ) -> anyhow::Result<CreateSprintResult> {
1314        self.create_sprint_tool
1315            .execute(params)
1316            .await
1317            .map_err(|e: JiraMcpError| {
1318                error!("create_sprint failed: {}", e);
1319                anyhow::anyhow!(e)
1320            })
1321    }
1322
1323    /// Start a sprint
1324    ///
1325    /// Transitions a future sprint to active state. Requires an end date to be set
1326    /// either on the sprint already or provided as a parameter.
1327    ///
1328    /// Validations:
1329    /// - Sprint must be in "future" state (not already active or closed)
1330    /// - End date must be set
1331    /// - Warns if sprint has no issues
1332    ///
1333    /// # Examples
1334    /// - Start with existing dates: `{"sprint_id": 123}`
1335    /// - Start and set end date: `{"sprint_id": 123, "end_date": "2025-02-03T23:59:59Z"}`
1336    /// - Start with custom start: `{"sprint_id": 123, "start_date": "2025-01-20T08:00:00Z", "end_date": "2025-02-03T18:00:00Z"}`
1337    #[instrument(skip(self))]
1338    pub async fn start_sprint(
1339        &self,
1340        params: StartSprintParams,
1341    ) -> anyhow::Result<StartSprintResult> {
1342        self.start_sprint_tool
1343            .execute(params)
1344            .await
1345            .map_err(|e: JiraMcpError| {
1346                error!("start_sprint failed: {}", e);
1347                anyhow::anyhow!(e)
1348            })
1349    }
1350
1351    /// Close a sprint
1352    ///
1353    /// Closes an active sprint and provides completion statistics. Optionally moves
1354    /// incomplete issues to another sprint for continuity.
1355    ///
1356    /// Features:
1357    /// - Calculates completion rate (done vs total issues)
1358    /// - Optionally moves incomplete issues to next sprint
1359    /// - Provides warnings about incomplete work
1360    /// - JIRA automatically sets complete date to current time
1361    ///
1362    /// # Examples
1363    /// - Simple close: `{"sprint_id": 123}`
1364    /// - Move incomplete to next: `{"sprint_id": 123, "move_incomplete_to": 124}`
1365    #[instrument(skip(self))]
1366    pub async fn close_sprint(
1367        &self,
1368        params: CloseSprintParams,
1369    ) -> anyhow::Result<CloseSprintResult> {
1370        self.close_sprint_tool
1371            .execute(params)
1372            .await
1373            .map_err(|e: JiraMcpError| {
1374                error!("close_sprint failed: {}", e);
1375                anyhow::anyhow!(e)
1376            })
1377    }
1378
1379    /// Link two issues together with a specific link type
1380    ///
1381    /// Creates a directional link between two issues. Common link types include:
1382    /// - "Blocks" / "is blocked by"
1383    /// - "Relates" / "relates to"
1384    /// - "Duplicates" / "is duplicated by"
1385    /// - "Clones" / "is cloned by"
1386    ///
1387    /// Use get_issue_link_types to see all available link types in your JIRA instance.
1388    ///
1389    /// # Examples
1390    /// - Link two issues: `{"inward_issue_key": "PROJ-123", "outward_issue_key": "PROJ-456", "link_type": "Blocks"}`
1391    /// - Link with comment: `{"inward_issue_key": "PROJ-123", "outward_issue_key": "PROJ-456", "link_type": "Relates", "comment": "These are related"}`
1392    #[instrument(skip(self))]
1393    pub async fn link_issues(&self, params: LinkIssuesParams) -> anyhow::Result<LinkIssuesResult> {
1394        self.link_issues_tool
1395            .execute(params)
1396            .await
1397            .map_err(|e: JiraMcpError| {
1398                error!("link_issues failed: {}", e);
1399                anyhow::anyhow!(e)
1400            })
1401    }
1402
1403    /// Delete an issue link
1404    ///
1405    /// Removes a link between two issues. You need the link ID (not issue keys).
1406    /// You can get link IDs from the issue_relationships tool or from issue details.
1407    ///
1408    /// # Examples
1409    /// - Delete a link: `{"link_id": "10001"}`
1410    #[instrument(skip(self))]
1411    pub async fn delete_issue_link(
1412        &self,
1413        params: DeleteIssueLinkParams,
1414    ) -> anyhow::Result<DeleteIssueLinkResult> {
1415        self.delete_issue_link_tool
1416            .execute(params)
1417            .await
1418            .map_err(|e: JiraMcpError| {
1419                error!("delete_issue_link failed: {}", e);
1420                anyhow::anyhow!(e)
1421            })
1422    }
1423
1424    /// Get all available issue link types
1425    ///
1426    /// Returns all link types configured in your JIRA instance, including their
1427    /// inward and outward descriptions. Use the "name" field when creating links.
1428    ///
1429    /// # Examples
1430    /// - Get all link types: `{}`
1431    #[instrument(skip(self))]
1432    pub async fn get_issue_link_types(&self) -> anyhow::Result<GetIssueLinkTypesResult> {
1433        self.get_issue_link_types_tool
1434            .execute()
1435            .await
1436            .map_err(|e: JiraMcpError| {
1437                error!("get_issue_link_types failed: {}", e);
1438                anyhow::anyhow!(e)
1439            })
1440    }
1441
1442    /// Manage labels on a JIRA issue
1443    ///
1444    /// Add, remove, or replace labels on an issue. Labels are useful for categorization,
1445    /// filtering, and organization. This tool supports:
1446    /// - Adding specific labels while keeping existing ones
1447    /// - Removing specific labels
1448    /// - Replacing all labels with a new set
1449    ///
1450    /// Use get_available_labels to see what labels are used in your project.
1451    ///
1452    /// # Examples
1453    /// - Add labels: `{"issue_key": "PROJ-123", "add_labels": ["urgent", "backend"]}`
1454    /// - Remove labels: `{"issue_key": "PROJ-123", "remove_labels": ["wontfix"]}`
1455    /// - Add and remove: `{"issue_key": "PROJ-123", "add_labels": ["reviewed"], "remove_labels": ["needs-review"]}`
1456    /// - Replace all: `{"issue_key": "PROJ-123", "add_labels": ["production", "critical"], "replace_all": true}`
1457    #[instrument(skip(self))]
1458    pub async fn manage_labels(
1459        &self,
1460        params: ManageLabelsParams,
1461    ) -> anyhow::Result<ManageLabelsResult> {
1462        self.labels_tool.manage_labels(params).await.map_err(|e| {
1463            error!("manage_labels failed: {}", e);
1464            anyhow::anyhow!(e)
1465        })
1466    }
1467
1468    /// Get available labels
1469    ///
1470    /// Returns a list of labels that are available or in use. Can be filtered by project
1471    /// to see only labels used in that project, or get all global labels.
1472    ///
1473    /// # Examples
1474    /// - Get all labels: `{}`
1475    /// - Get labels for project: `{"project_key": "PROJ"}`
1476    /// - Get with pagination: `{"max_results": 50, "start_at": 0}`
1477    #[instrument(skip(self))]
1478    pub async fn get_available_labels(
1479        &self,
1480        params: GetAvailableLabelsParams,
1481    ) -> anyhow::Result<GetAvailableLabelsResult> {
1482        self.labels_tool
1483            .get_available_labels(params)
1484            .await
1485            .map_err(|e| {
1486                error!("get_available_labels failed: {}", e);
1487                anyhow::anyhow!(e)
1488            })
1489    }
1490
1491    /// Update components on a JIRA issue
1492    ///
1493    /// Sets the components for an issue. Components represent subsystems or categories
1494    /// within a project (e.g., "Backend", "Frontend", "API", "Database").
1495    /// This operation replaces all existing components.
1496    ///
1497    /// Use get_available_components to see what components are available in your project.
1498    ///
1499    /// # Examples
1500    /// - Set components: `{"issue_key": "PROJ-123", "components": ["Backend", "API"]}`
1501    /// - Clear components: `{"issue_key": "PROJ-123", "components": []}`
1502    /// - Set by ID: `{"issue_key": "PROJ-123", "components": ["10000", "10001"]}`
1503    #[instrument(skip(self))]
1504    pub async fn update_components(
1505        &self,
1506        params: UpdateComponentsParams,
1507    ) -> anyhow::Result<UpdateComponentsResult> {
1508        self.components_tool
1509            .update_components(params)
1510            .await
1511            .map_err(|e| {
1512                error!("update_components failed: {}", e);
1513                anyhow::anyhow!(e)
1514            })
1515    }
1516
1517    /// Get available components for a project
1518    ///
1519    /// Returns all components configured for a specific project. Components represent
1520    /// subsystems or categories and can be used to organize and filter issues.
1521    ///
1522    /// # Examples
1523    /// - Get project components: `{"project_key": "PROJ"}`
1524    #[instrument(skip(self))]
1525    pub async fn get_available_components(
1526        &self,
1527        params: GetAvailableComponentsParams,
1528    ) -> anyhow::Result<GetAvailableComponentsResult> {
1529        self.components_tool
1530            .get_available_components(params)
1531            .await
1532            .map_err(|e| {
1533                error!("get_available_components failed: {}", e);
1534                anyhow::anyhow!(e)
1535            })
1536    }
1537
1538    /// Bulk create multiple JIRA issues
1539    ///
1540    /// Creates multiple issues in a single operation with parallel execution for improved
1541    /// performance. This tool supports creating many issues at once while handling partial
1542    /// failures gracefully. Includes automatic retry logic for rate limits.
1543    ///
1544    /// Key features:
1545    /// - Parallel execution with configurable concurrency (default: 5, max: 20)
1546    /// - Automatic retry with exponential backoff for rate limits (default: 3 retries)
1547    /// - Continues on errors by default (configurable with stop_on_error)
1548    /// - Returns detailed results for each issue (success or failure)
1549    /// - Reuses CreateIssueParams structure for consistency
1550    ///
1551    /// Performance: 70-85% faster than sequential operations
1552    /// Large batches (100+): For batches over 100 items, consider splitting into smaller chunks
1553    /// of 50-100 items with delays between batches to avoid overwhelming JIRA
1554    ///
1555    /// # Examples
1556    /// - Create multiple tasks: `{"project_key": "PROJ", "issues": [{"summary": "Task 1"}, {"summary": "Task 2"}]}`
1557    /// - With custom concurrency: `{"project_key": "PROJ", "issues": [...], "max_concurrent": 10}`
1558    /// - With retry config: `{"project_key": "PROJ", "issues": [...], "max_retries": 5, "initial_retry_delay_ms": 2000}`
1559    ///
1560    /// Note: initial_retry_delay_ms has a minimum of 500ms to prevent API hammering
1561    /// - Stop on error: `{"project_key": "PROJ", "issues": [...], "stop_on_error": true}`
1562    #[instrument(skip(self))]
1563    pub async fn bulk_create_issues(
1564        &self,
1565        params: BulkCreateIssuesParams,
1566    ) -> anyhow::Result<BulkCreateIssuesResult> {
1567        self.bulk_operations_tool
1568            .bulk_create_issues(params)
1569            .await
1570            .map_err(|e| {
1571                error!("bulk_create_issues failed: {}", e);
1572                anyhow::anyhow!(e)
1573            })
1574    }
1575
1576    /// Bulk transition multiple issues to a new status
1577    ///
1578    /// Transitions multiple issues to the same status in parallel. Perfect for batch
1579    /// status updates, moving multiple issues through workflow stages, or bulk closing issues.
1580    /// Includes automatic retry logic for rate limits.
1581    ///
1582    /// Key features:
1583    /// - Parallel execution with configurable concurrency (default: 5, max: 20)
1584    /// - Automatic retry with exponential backoff (default: 3 retries)
1585    /// - Single transition applied to all issues
1586    /// - Optional comment and resolution (same for all issues)
1587    /// - Detailed success/failure reporting per issue
1588    ///
1589    /// # Examples
1590    /// - Transition by name: `{"issue_keys": ["PROJ-1", "PROJ-2"], "transition_name": "Done"}`
1591    /// - Transition by ID: `{"issue_keys": ["PROJ-1", "PROJ-2"], "transition_id": "31"}`
1592    /// - With comment: `{"issue_keys": [...], "transition_name": "In Progress", "comment": "Starting work"}`
1593    /// - With resolution: `{"issue_keys": [...], "transition_name": "Done", "resolution": "Fixed"}`
1594    /// - With retry config: `{"issue_keys": [...], "transition_name": "Done", "max_retries": 5}`
1595    #[instrument(skip(self))]
1596    pub async fn bulk_transition_issues(
1597        &self,
1598        params: BulkTransitionIssuesParams,
1599    ) -> anyhow::Result<BulkTransitionIssuesResult> {
1600        self.bulk_operations_tool
1601            .bulk_transition_issues(params)
1602            .await
1603            .map_err(|e| {
1604                error!("bulk_transition_issues failed: {}", e);
1605                anyhow::anyhow!(e)
1606            })
1607    }
1608
1609    /// Bulk update fields on multiple issues
1610    ///
1611    /// Updates the same field values on multiple issues in parallel. Useful for bulk
1612    /// updates of priority, labels, custom fields, or any other field. Includes automatic retry.
1613    ///
1614    /// Key features:
1615    /// - Parallel execution with configurable concurrency (default: 5, max: 20)
1616    /// - Automatic retry with exponential backoff (default: 3 retries)
1617    /// - Same field updates applied to all issues
1618    /// - Supports any JIRA field (standard or custom)
1619    /// - Detailed success/failure reporting per issue
1620    ///
1621    /// # Examples
1622    /// - Update priority: `{"issue_keys": ["PROJ-1", "PROJ-2"], "field_updates": {"priority": {"name": "High"}}}`
1623    /// - Update custom field: `{"issue_keys": [...], "field_updates": {"customfield_10050": "value"}}`
1624    /// - Multiple fields: `{"issue_keys": [...], "field_updates": {"priority": {"name": "High"}, "labels": ["urgent"]}}`
1625    #[instrument(skip(self))]
1626    pub async fn bulk_update_fields(
1627        &self,
1628        params: BulkUpdateFieldsParams,
1629    ) -> anyhow::Result<BulkUpdateFieldsResult> {
1630        self.bulk_operations_tool
1631            .bulk_update_fields(params)
1632            .await
1633            .map_err(|e| {
1634                error!("bulk_update_fields failed: {}", e);
1635                anyhow::anyhow!(e)
1636            })
1637    }
1638
1639    /// Bulk assign multiple issues to a user
1640    ///
1641    /// Assigns multiple issues to the same user in parallel. Great for bulk task
1642    /// assignment, team distribution, or unassigning multiple issues. Includes automatic retry.
1643    ///
1644    /// Key features:
1645    /// - Parallel execution with configurable concurrency (default: 5, max: 20)
1646    /// - Automatic retry with exponential backoff (default: 3 retries)
1647    /// - Supports "me" for current user
1648    /// - Can unassign by providing null/empty assignee
1649    /// - Detailed success/failure reporting per issue
1650    ///
1651    /// # Examples
1652    /// - Assign to self: `{"issue_keys": ["PROJ-1", "PROJ-2"], "assignee": "me"}`
1653    /// - Assign to user: `{"issue_keys": [...], "assignee": "user@example.com"}`
1654    /// - Unassign all: `{"issue_keys": [...], "assignee": null}`
1655    #[instrument(skip(self))]
1656    pub async fn bulk_assign_issues(
1657        &self,
1658        params: BulkAssignIssuesParams,
1659    ) -> anyhow::Result<BulkAssignIssuesResult> {
1660        self.bulk_operations_tool
1661            .bulk_assign_issues(params)
1662            .await
1663            .map_err(|e| {
1664                error!("bulk_assign_issues failed: {}", e);
1665                anyhow::anyhow!(e)
1666            })
1667    }
1668
1669    /// Bulk add or remove labels from multiple issues
1670    ///
1671    /// Updates labels on multiple issues in parallel. Can add labels, remove labels,
1672    /// or both in a single operation. Includes automatic retry logic.
1673    ///
1674    /// Key features:
1675    /// - Parallel execution with configurable concurrency (default: 5, max: 20)
1676    /// - Automatic retry with exponential backoff (default: 3 retries)
1677    /// - Add and/or remove labels in one operation
1678    /// - Same label changes applied to all issues
1679    /// - Detailed success/failure reporting per issue
1680    ///
1681    /// # Examples
1682    /// - Add labels: `{"issue_keys": ["PROJ-1", "PROJ-2"], "add_labels": ["urgent", "backend"]}`
1683    /// - Remove labels: `{"issue_keys": [...], "remove_labels": ["wontfix"]}`
1684    /// - Add and remove: `{"issue_keys": [...], "add_labels": ["reviewed"], "remove_labels": ["needs-review"]}`
1685    #[instrument(skip(self))]
1686    pub async fn bulk_add_labels(
1687        &self,
1688        params: BulkAddLabelsParams,
1689    ) -> anyhow::Result<BulkAddLabelsResult> {
1690        self.bulk_operations_tool
1691            .bulk_add_labels(params)
1692            .await
1693            .map_err(|e| {
1694                error!("bulk_add_labels failed: {}", e);
1695                anyhow::anyhow!(e)
1696            })
1697    }
1698}
1699
1700// Add any additional implementation methods here that are NOT MCP tools
1701impl JiraMcpServer {
1702    /// Internal method to refresh current user cache
1703    #[allow(dead_code)]
1704    async fn refresh_current_user_cache(&self) -> JiraMcpResult<()> {
1705        match self.jira_client.get_current_user().await {
1706            Ok(user) => {
1707                let user_mapping = UserMapping {
1708                    account_id: user.account_id,
1709                    display_name: user.display_name,
1710                    email_address: user.email_address,
1711                    username: None,
1712                };
1713                self.cache.set_current_user(user_mapping)
1714            }
1715            Err(e) => Err(e),
1716        }
1717    }
1718
1719    /// Internal method to validate tool parameters (common validations)
1720    #[allow(dead_code)]
1721    fn validate_common_params(
1722        &self,
1723        limit: Option<u32>,
1724        start_at: Option<u32>,
1725    ) -> JiraMcpResult<()> {
1726        if let Some(limit) = limit {
1727            if limit == 0 {
1728                return Err(JiraMcpError::invalid_param(
1729                    "limit",
1730                    "Limit must be greater than 0",
1731                ));
1732            }
1733            if limit > 200 {
1734                return Err(JiraMcpError::invalid_param(
1735                    "limit",
1736                    "Limit cannot exceed 200",
1737                ));
1738            }
1739        }
1740
1741        if let Some(start_at) = start_at {
1742            if start_at > 10000 {
1743                return Err(JiraMcpError::invalid_param(
1744                    "start_at",
1745                    "start_at cannot exceed 10000",
1746                ));
1747            }
1748        }
1749
1750        Ok(())
1751    }
1752}
1753
1754#[cfg(test)]
1755mod tests {
1756    use super::*;
1757    use crate::config::{AuthConfig, JiraConfig};
1758
1759    // Note: These tests require a real JIRA instance for integration testing
1760    // Unit tests are included in individual modules
1761
1762    #[tokio::test]
1763    async fn test_server_creation_with_invalid_config() {
1764        let config = JiraConfig {
1765            jira_url: "invalid-url".to_string(),
1766            auth: AuthConfig::Anonymous,
1767            ..Default::default()
1768        };
1769
1770        // Should fail validation
1771        assert!(JiraMcpServer::with_config(config).await.is_err());
1772    }
1773
1774    #[test]
1775    fn test_uptime_calculation() {
1776        let start_time = Instant::now();
1777        // Sleep is not needed for this test, just checking the calculation
1778        let elapsed = start_time.elapsed().as_secs();
1779        // elapsed is u64, which is always >= 0, so we just check it's a reasonable value
1780        assert!(elapsed < 10); // Should be very small since we just started
1781    }
1782}