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}