Skip to content

Commit 344aad6

Browse files
committed
feat(jira): add custom field options MCP tools
Add three new MCP tools for retrieving Jira custom field options: - jira_get_customfield_options: Get global options for a custom field - jira_get_customfield_contexts: Get contexts for a custom field - jira_get_customfield_context_options: Get context-specific options Features: - Cross-platform support (Cloud and Server/DC with appropriate API versions) - Comprehensive input validation and error handling - Pagination support with configurable limits - Detailed logging and debugging capabilities - Full authentication method support (API tokens, PAT, OAuth) Implementation: - New Pydantic models for field options, contexts, and responses - API client methods in FieldsMixin with Cloud/Server detection - MCP tool implementations with comprehensive parameter documentation - Unit tests covering models, validation, and API version selection - Updated documentation with new tools in README Technical fixes: - Use from_api_response() methods in API client for proper response parsing - Handle both Cloud ('values') and DC/Server ('options') API response formats - Robust pagination handling with fallbacks for missing DC/Server fields - Enhanced error resilience for malformed responses This enables AI assistants to discover available custom field options before creating/updating Jira issues, improving automation accuracy.
1 parent 9ad2cbf commit 344aad6

File tree

6 files changed

+1048
-0
lines changed

6 files changed

+1048
-0
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,9 @@ Here's a complete example of setting up multi-user authentication with streamabl
730730
- `jira_update_issue`: Update an existing issue
731731
- `jira_transition_issue`: Transition an issue to a new status
732732
- `jira_add_comment`: Add a comment to an issue
733+
- `jira_get_customfield_options`: Get available options for custom fields
734+
- `jira_get_customfield_contexts`: Get contexts for custom fields
735+
- `jira_get_customfield_context_options`: Get options for custom fields within specific contexts
733736
734737
#### Confluence Tools
735738
@@ -749,6 +752,9 @@ Here's a complete example of setting up multi-user authentication with streamabl
749752
| | `jira_get_worklog` | `confluence_get_labels` |
750753
| | `jira_get_transitions` | `confluence_search_user` |
751754
| | `jira_search_fields` | |
755+
| | `jira_get_customfield_options` | |
756+
| | `jira_get_customfield_contexts` | |
757+
| | `jira_get_customfield_context_options` | |
752758
| | `jira_get_agile_boards` | |
753759
| | `jira_get_board_issues` | |
754760
| | `jira_get_sprints_from_board` | |

src/mcp_atlassian/jira/fields.py

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55

66
from thefuzz import fuzz
77

8+
from mcp_atlassian.models.jira.field_option import (
9+
JiraFieldContextsResponse,
10+
JiraFieldOptionsResponse,
11+
JiraFieldContextOptionsResponse,
12+
)
813
from .client import JiraClient
914
from .protocols import EpicOperationsProto, UsersOperationsProto
1015

@@ -524,3 +529,213 @@ def similarity(keyword: str, field: dict) -> int:
524529
except Exception as e:
525530
logger.error(f"Error searching fields: {str(e)}")
526531
return []
532+
533+
def get_field_contexts(
534+
self,
535+
field_id: str,
536+
start_at: int = 0,
537+
max_results: int = 10000,
538+
) -> JiraFieldContextsResponse:
539+
"""
540+
Get contexts for a custom field.
541+
542+
Args:
543+
field_id: The ID of the field (e.g., 'customfield_10001')
544+
start_at: Starting index for pagination (default: 0)
545+
max_results: Maximum number of results per page (default: 10000)
546+
547+
Returns:
548+
JiraFieldContextsResponse with contexts for the field
549+
550+
Raises:
551+
ValueError: If the field_id is not provided or invalid
552+
"""
553+
if not field_id:
554+
raise ValueError("Field ID is required")
555+
556+
if not field_id.startswith("customfield_"):
557+
raise ValueError("Field ID must be a custom field (starting with 'customfield_')")
558+
559+
try:
560+
logger.debug(f"Getting contexts for field '{field_id}'")
561+
562+
# Use different API endpoints for Cloud vs DC/Server
563+
if self.config.is_cloud:
564+
# Cloud API
565+
path = f"/rest/api/3/field/{field_id}/context"
566+
else:
567+
# DC/Server API - contexts endpoint may not exist or be different
568+
# For DC, we'll use the same endpoint as Cloud but with API v2
569+
path = f"/rest/api/2/field/{field_id}/context"
570+
571+
params = {
572+
"startAt": start_at,
573+
"maxResults": max_results,
574+
}
575+
576+
result = self.jira.get(
577+
path=path,
578+
params=params,
579+
)
580+
581+
if not isinstance(result, dict):
582+
error_msg = f"Unexpected response type from field contexts API: {type(result)}"
583+
logger.error(error_msg)
584+
raise ValueError(error_msg)
585+
586+
# Parse the response using our model
587+
contexts_response = JiraFieldContextsResponse.from_api_response(
588+
result,
589+
max_results=max_results
590+
)
591+
logger.debug(
592+
f"Retrieved {len(contexts_response.values)} contexts for field '{field_id}'"
593+
)
594+
return contexts_response
595+
596+
except Exception as e:
597+
logger.error(f"Error getting contexts for field '{field_id}': {str(e)}")
598+
raise
599+
600+
def get_field_options(
601+
self,
602+
field_id: str,
603+
start_at: int = 0,
604+
max_results: int = 10000,
605+
) -> JiraFieldOptionsResponse:
606+
"""
607+
Get options for a custom field (global options).
608+
609+
Args:
610+
field_id: The ID of the field (e.g., 'customfield_10001')
611+
start_at: Starting index for pagination (default: 0)
612+
max_results: Maximum number of results per page (default: 10000)
613+
614+
Returns:
615+
JiraFieldOptionsResponse with options for the field
616+
617+
Raises:
618+
ValueError: If the field_id is not provided or invalid
619+
"""
620+
if not field_id:
621+
raise ValueError("Field ID is required")
622+
623+
if not field_id.startswith("customfield_"):
624+
raise ValueError("Field ID must be a custom field (starting with 'customfield_')")
625+
626+
try:
627+
logger.debug(f"Getting global options for field '{field_id}'")
628+
629+
# Use different API endpoints for Cloud vs DC/Server
630+
if self.config.is_cloud:
631+
# Cloud API - different endpoint structure
632+
path = f"/rest/api/3/field/{field_id}/option"
633+
else:
634+
# DC/Server API - uses customFields endpoint with numerical ID only
635+
# Extract numerical ID from customfield_XXXXX
636+
numerical_id = field_id.replace("customfield_", "")
637+
path = f"/rest/api/2/customFields/{numerical_id}/options"
638+
639+
params = {
640+
"startAt": start_at,
641+
"maxResults": max_results,
642+
}
643+
644+
result = self.jira.get(
645+
path=path,
646+
params=params,
647+
)
648+
649+
if not isinstance(result, dict):
650+
error_msg = f"Unexpected response type from field options API: {type(result)}"
651+
logger.error(error_msg)
652+
raise ValueError(error_msg)
653+
654+
# Parse the response using our model
655+
options_response = JiraFieldOptionsResponse.from_api_response(
656+
result,
657+
max_results=max_results
658+
)
659+
logger.debug(
660+
f"Retrieved {len(options_response.values)} options for field '{field_id}'"
661+
)
662+
return options_response
663+
664+
except Exception as e:
665+
logger.error(f"Error getting options for field '{field_id}': {str(e)}")
666+
raise
667+
668+
def get_field_context_options(
669+
self,
670+
field_id: str,
671+
context_id: str,
672+
start_at: int = 0,
673+
max_results: int = 10000,
674+
) -> JiraFieldContextOptionsResponse:
675+
"""
676+
Get options for a custom field within a specific context.
677+
This is the most precise way to get field options as they can differ by context.
678+
679+
Args:
680+
field_id: The ID of the field (e.g., 'customfield_10001')
681+
context_id: The ID of the context
682+
start_at: Starting index for pagination (default: 0)
683+
max_results: Maximum number of results per page (default: 10000)
684+
685+
Returns:
686+
JiraFieldContextOptionsResponse with options for the field in the specified context
687+
688+
Raises:
689+
ValueError: If the field_id or context_id is not provided or invalid
690+
"""
691+
if not field_id:
692+
raise ValueError("Field ID is required")
693+
if not context_id:
694+
raise ValueError("Context ID is required")
695+
696+
if not field_id.startswith("customfield_"):
697+
raise ValueError("Field ID must be a custom field (starting with 'customfield_')")
698+
699+
try:
700+
logger.debug(f"Getting context options for field '{field_id}' in context '{context_id}'")
701+
702+
# Use different API endpoints for Cloud vs DC/Server
703+
if self.config.is_cloud:
704+
# Cloud API
705+
path = f"/rest/api/3/field/{field_id}/context/{context_id}/option"
706+
else:
707+
# DC/Server API - context-specific options may not be available
708+
# Fall back to general options endpoint with numerical ID only
709+
# Extract numerical ID from customfield_XXXXX
710+
numerical_id = field_id.replace("customfield_", "")
711+
path = f"/rest/api/2/customFields/{numerical_id}/options"
712+
logger.warning(f"DC/Server may not support context-specific options for field '{field_id}', using general options")
713+
714+
params = {
715+
"startAt": start_at,
716+
"maxResults": max_results,
717+
}
718+
719+
result = self.jira.get(
720+
path=path,
721+
params=params,
722+
)
723+
724+
if not isinstance(result, dict):
725+
error_msg = f"Unexpected response type from field context options API: {type(result)}"
726+
logger.error(error_msg)
727+
raise ValueError(error_msg)
728+
729+
# Parse the response using our model
730+
context_options_response = JiraFieldContextOptionsResponse.from_api_response(
731+
result,
732+
max_results=max_results
733+
)
734+
logger.debug(
735+
f"Retrieved {len(context_options_response.values)} options for field '{field_id}' in context '{context_id}'"
736+
)
737+
return context_options_response
738+
739+
except Exception as e:
740+
logger.error(f"Error getting context options for field '{field_id}' in context '{context_id}': {str(e)}")
741+
raise

src/mcp_atlassian/models/jira/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@
2525
JiraLinkedIssueFields,
2626
)
2727
from .project import JiraProject
28+
from .field_option import (
29+
JiraFieldOption,
30+
JiraFieldContext,
31+
JiraFieldOptionsResponse,
32+
JiraFieldContextOptionsResponse,
33+
JiraFieldContextsResponse,
34+
)
2835
from .search import JiraSearchResult
2936
from .workflow import JiraTransition
3037
from .worklog import JiraWorklog
@@ -52,4 +59,10 @@
5259
"JiraIssueLink",
5360
"JiraLinkedIssue",
5461
"JiraLinkedIssueFields",
62+
# Field options models
63+
"JiraFieldOption",
64+
"JiraFieldContext",
65+
"JiraFieldOptionsResponse",
66+
"JiraFieldContextOptionsResponse",
67+
"JiraFieldContextsResponse",
5568
]

0 commit comments

Comments
 (0)