Coverage for src/mcp_atlassian/server.py: 22%
380 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-10 03:26 +0900
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-10 03:26 +0900
1import json
2import logging
3import os
4from collections.abc import Callable, Sequence
5from typing import Any
7from mcp.server import Server
8from mcp.types import Resource, TextContent, Tool
9from pydantic import AnyUrl
11from .confluence import ConfluenceFetcher
12from .document_types import Document
13from .jira import JiraFetcher
14from .preprocessing import markdown_to_confluence_storage
16# Configure logging
17logging.basicConfig(
18 level=logging.INFO,
19 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
20 filename="mcp_atlassian_debug.log",
21 filemode="a",
22)
23logger = logging.getLogger("mcp-atlassian")
24logging.getLogger("mcp.server.lowlevel.server").setLevel(logging.INFO)
26# Type aliases for formatter functions
27CommentFormatter = Callable[[dict[str, Any]], dict[str, Any]]
28IssueFormatter = Callable[[Document], dict[str, Any]]
29TransitionFormatter = Callable[[dict[str, Any]], dict[str, Any]]
32def get_available_services() -> dict[str, bool | None]:
33 """Determine which services are available based on environment variables."""
34 confluence_vars = all(
35 [
36 os.getenv("CONFLUENCE_URL"),
37 os.getenv("CONFLUENCE_USERNAME"),
38 os.getenv("CONFLUENCE_API_TOKEN"),
39 ]
40 )
42 # Check for either cloud authentication (URL + username + API token)
43 # or server/data center authentication (URL + personal token)
44 jira_url = os.getenv("JIRA_URL")
45 if jira_url:
46 is_cloud = "atlassian.net" in jira_url
47 if is_cloud:
48 jira_vars = all(
49 [jira_url, os.getenv("JIRA_USERNAME"), os.getenv("JIRA_API_TOKEN")]
50 )
51 logger.info("Using Jira Cloud authentication method")
52 else:
53 jira_vars = all([jira_url, os.getenv("JIRA_PERSONAL_TOKEN")])
54 logger.info("Using Jira Server/Data Center authentication method")
55 else:
56 jira_vars = False
58 return {"confluence": confluence_vars, "jira": jira_vars}
61# Initialize services based on available credentials
62services = get_available_services()
63confluence_fetcher = ConfluenceFetcher() if services["confluence"] else None
64jira_fetcher = JiraFetcher() if services["jira"] else None
65app = Server("mcp-atlassian")
68@app.list_resources()
69async def list_resources() -> list[Resource]:
70 """List Confluence spaces and Jira projects the user is actively interacting with."""
71 resources = []
73 # Add Confluence spaces the user has contributed to
74 if confluence_fetcher:
75 try:
76 # Get spaces the user has contributed to
77 spaces = confluence_fetcher.get_user_contributed_spaces(limit=250)
79 # Add spaces to resources
80 resources.extend(
81 [
82 Resource(
83 uri=AnyUrl(f"confluence://{space['key']}"),
84 name=f"Confluence Space: {space['name']}",
85 mimeType="text/plain",
86 description=(
87 f"A Confluence space containing documentation and knowledge base articles. "
88 f"Space Key: {space['key']}. "
89 f"{space.get('description', '')} "
90 f"Access content using: confluence://{space['key']}/pages/PAGE_TITLE"
91 ).strip(),
92 )
93 for space in spaces.values()
94 ]
95 )
96 except KeyError as e:
97 logger.error(f"Missing key in Confluence spaces data: {str(e)}")
98 except ValueError as e:
99 logger.error(f"Invalid value in Confluence spaces: {str(e)}")
100 except TypeError as e:
101 logger.error(f"Type error when processing Confluence spaces: {str(e)}")
102 except Exception as e: # noqa: BLE001 - Intentional fallback with logging
103 logger.error(f"Unexpected error fetching Confluence spaces: {str(e)}")
104 logger.debug("Full exception details for Confluence spaces:", exc_info=True)
106 # Add Jira projects the user is involved with
107 if jira_fetcher:
108 try:
109 # Get current user's account ID
110 account_id = jira_fetcher.get_current_user_account_id()
112 # Use JQL to find issues the user is assigned to or reported
113 jql = f"assignee = {account_id} OR reporter = {account_id} ORDER BY updated DESC"
114 issues = jira_fetcher.jira.jql(jql, limit=250, fields=["project"])
116 # Extract and deduplicate projects
117 projects = {}
118 for issue in issues.get("issues", []):
119 project = issue.get("fields", {}).get("project", {})
120 project_key = project.get("key")
121 if project_key and project_key not in projects:
122 projects[project_key] = {
123 "key": project_key,
124 "name": project.get("name", project_key),
125 "description": project.get("description", ""),
126 }
128 # Add projects to resources
129 resources.extend(
130 [
131 Resource(
132 uri=AnyUrl(f"jira://{project['key']}"),
133 name=f"Jira Project: {project['name']}",
134 mimeType="text/plain",
135 description=(
136 f"A Jira project tracking issues and tasks. Project Key: {project['key']}. "
137 ).strip(),
138 )
139 for project in projects.values()
140 ]
141 )
142 except KeyError as e:
143 logger.error(f"Missing key in Jira projects data: {str(e)}")
144 except ValueError as e:
145 logger.error(f"Invalid value in Jira projects: {str(e)}")
146 except TypeError as e:
147 logger.error(f"Type error when processing Jira projects: {str(e)}")
148 except Exception as e: # noqa: BLE001 - Intentional fallback with logging
149 logger.error(f"Unexpected error fetching Jira projects: {str(e)}")
150 logger.debug("Full exception details for Jira projects:", exc_info=True)
152 return resources
155@app.read_resource()
156async def read_resource(uri: AnyUrl) -> str:
157 """Read content from Confluence or Jira."""
158 uri_str = str(uri)
160 # Handle Confluence resources
161 if uri_str.startswith("confluence://"):
162 return _handle_confluence_resource(uri_str)
164 # Handle Jira resources
165 elif uri_str.startswith("jira://"):
166 return _handle_jira_resource(uri_str)
168 # Invalid resource URI
169 error_msg = f"Invalid resource URI: {uri}"
170 raise ValueError(error_msg)
173def _handle_confluence_resource(uri_str: str) -> str:
174 """
175 Handle reading Confluence resources.
177 Args:
178 uri_str: The URI string for the Confluence resource
180 Returns:
181 The content of the resource
183 Raises:
184 ValueError: If Confluence is not configured or the resource is not found
185 """
186 if not services["confluence"]:
187 error_msg = (
188 "Confluence is not configured. Please provide Confluence credentials."
189 )
190 raise ValueError(error_msg)
192 parts = uri_str.replace("confluence://", "").split("/")
194 # Handle space listing
195 if len(parts) == 1:
196 return _handle_confluence_space(parts[0])
198 # Handle specific page
199 elif len(parts) >= 3 and parts[1] == "pages":
200 return _handle_confluence_page(parts[0], parts[2])
202 # Invalid Confluence resource
203 error_msg = f"Invalid Confluence resource URI: {uri_str}"
204 raise ValueError(error_msg)
207def _handle_confluence_space(space_key: str) -> str:
208 """
209 Handle reading a Confluence space.
211 Args:
212 space_key: The key of the space to read
214 Returns:
215 Formatted content of pages in the space
216 """
217 # Use CQL to find recently updated pages in this space
218 cql = f'space = "{space_key}" AND contributor = currentUser() ORDER BY lastmodified DESC'
219 documents = confluence_fetcher.search(cql=cql, limit=20)
221 if not documents:
222 # Fallback to regular space pages if no user-contributed pages found
223 documents = confluence_fetcher.get_space_pages(
224 space_key, limit=10, convert_to_markdown=True
225 )
227 content = []
228 for doc in documents:
229 title = doc.metadata.get("title", "Untitled")
230 url = doc.metadata.get("url", "")
231 content.append(f"# [{title}]({url})\n\n{doc.page_content}\n\n---")
233 return "\n\n".join(content)
236def _handle_confluence_page(space_key: str, title: str) -> str:
237 """
238 Handle reading a specific Confluence page.
240 Args:
241 space_key: The key of the space containing the page
242 title: The title of the page to read
244 Returns:
245 Content of the page
247 Raises:
248 ValueError: If the page is not found
249 """
250 doc = confluence_fetcher.get_page_by_title(space_key, title)
251 if not doc:
252 error_msg = f"Page not found: {title}"
253 raise ValueError(error_msg)
254 return doc.page_content
257def _handle_jira_resource(uri_str: str) -> str:
258 """
259 Handle reading Jira resources.
261 Args:
262 uri_str: The URI string for the Jira resource
264 Returns:
265 The content of the resource
267 Raises:
268 ValueError: If Jira is not configured or the resource is not found
269 """
270 if not services["jira"]:
271 error_msg = "Jira is not configured. Please provide Jira credentials."
272 raise ValueError(error_msg)
274 parts = uri_str.replace("jira://", "").split("/")
276 # Handle project listing
277 if len(parts) == 1:
278 return _handle_jira_project(parts[0])
280 # Handle specific issue
281 elif len(parts) >= 3 and parts[1] == "issues":
282 return _handle_jira_issue(parts[2])
284 # Invalid Jira resource
285 error_msg = f"Invalid Jira resource URI: {uri_str}"
286 raise ValueError(error_msg)
289def _handle_jira_project(project_key: str) -> str:
290 """
291 Handle reading a Jira project.
293 Args:
294 project_key: The key of the project to read
296 Returns:
297 Formatted content of issues in the project
298 """
299 # Get current user's account ID
300 account_id = jira_fetcher.get_current_user_account_id()
302 # Use JQL to find issues in this project that the user is involved with
303 jql = f"project = {project_key} AND (assignee = {account_id} OR reporter = {account_id}) ORDER BY updated DESC"
304 issues = jira_fetcher.search_issues(jql=jql, limit=20)
306 if not issues:
307 # Fallback to recent issues if no user-related issues found
308 issues = jira_fetcher.get_project_issues(project_key, limit=10)
310 content = []
311 for issue in issues:
312 key = issue.metadata.get("key", "")
313 title = issue.metadata.get("title", "")
314 url = issue.metadata.get("url", "")
315 status = issue.metadata.get("status", "")
316 content.append(
317 f"# [{key}: {title}]({url})\nStatus: {status}\n\n{issue.page_content}\n\n---"
318 )
320 return "\n\n".join(content)
323def _handle_jira_issue(issue_key: str) -> str:
324 """
325 Handle reading a specific Jira issue.
327 Args:
328 issue_key: The key of the issue to read
330 Returns:
331 Content of the issue
332 """
333 issue = jira_fetcher.get_issue(issue_key)
334 return issue.page_content
337@app.list_tools()
338async def list_tools() -> list[Tool]:
339 """List available Confluence and Jira tools."""
340 tools = []
342 if confluence_fetcher:
343 tools.extend(
344 [
345 Tool(
346 name="confluence_search",
347 description="Search Confluence content using CQL",
348 inputSchema={
349 "type": "object",
350 "properties": {
351 "query": {
352 "type": "string",
353 "description": "CQL query string (e.g. 'type=page AND space=DEV')",
354 },
355 "limit": {
356 "type": "number",
357 "description": "Maximum number of results (1-50)",
358 "default": 10,
359 "minimum": 1,
360 "maximum": 50,
361 },
362 },
363 "required": ["query"],
364 },
365 ),
366 Tool(
367 name="confluence_get_page",
368 description="Get content of a specific Confluence page by ID",
369 inputSchema={
370 "type": "object",
371 "properties": {
372 "page_id": {
373 "type": "string",
374 "description": "Confluence page ID (numeric ID, can be parsed from URL, e.g. from 'https://example.atlassian.net/wiki/spaces/TEAM/pages/123456789/Page+Title' -> '123456789')",
375 },
376 "include_metadata": {
377 "type": "boolean",
378 "description": "Whether to include page metadata",
379 "default": True,
380 },
381 },
382 "required": ["page_id"],
383 },
384 ),
385 Tool(
386 name="confluence_get_comments",
387 description="Get comments for a specific Confluence page",
388 inputSchema={
389 "type": "object",
390 "properties": {
391 "page_id": {
392 "type": "string",
393 "description": "Confluence page ID (numeric ID, can be parsed from URL, e.g. from 'https://example.atlassian.net/wiki/spaces/TEAM/pages/123456789/Page+Title' -> '123456789')",
394 }
395 },
396 "required": ["page_id"],
397 },
398 ),
399 Tool(
400 name="confluence_create_page",
401 description="Create a new Confluence page",
402 inputSchema={
403 "type": "object",
404 "properties": {
405 "space_key": {
406 "type": "string",
407 "description": "The key of the space to create the page in",
408 },
409 "title": {
410 "type": "string",
411 "description": "The title of the page",
412 },
413 "content": {
414 "type": "string",
415 "description": "The content of the page in Markdown format",
416 },
417 "parent_id": {
418 "type": "string",
419 "description": "Optional parent page ID",
420 },
421 },
422 "required": ["space_key", "title", "content"],
423 },
424 ),
425 Tool(
426 name="confluence_update_page",
427 description="Update an existing Confluence page",
428 inputSchema={
429 "type": "object",
430 "properties": {
431 "page_id": {
432 "type": "string",
433 "description": "The ID of the page to update",
434 },
435 "title": {
436 "type": "string",
437 "description": "The new title of the page",
438 },
439 "content": {
440 "type": "string",
441 "description": "The new content of the page in Markdown format",
442 },
443 "minor_edit": {
444 "type": "boolean",
445 "description": "Whether this is a minor edit",
446 "default": False,
447 },
448 "version_comment": {
449 "type": "string",
450 "description": "Optional comment for this version",
451 "default": "",
452 },
453 },
454 "required": ["page_id", "title", "content"],
455 },
456 ),
457 ]
458 )
460 if jira_fetcher:
461 tools.extend(
462 [
463 Tool(
464 name="jira_get_issue",
465 description="Get details of a specific Jira issue including its Epic links and relationship information",
466 inputSchema={
467 "type": "object",
468 "properties": {
469 "issue_key": {
470 "type": "string",
471 "description": "Jira issue key (e.g., 'PROJ-123')",
472 },
473 "expand": {
474 "type": "string",
475 "description": "Optional fields to expand. Examples: 'renderedFields' (for rendered content), 'transitions' (for available status transitions), 'changelog' (for history)",
476 "default": None,
477 },
478 "comment_limit": {
479 "type": "integer",
480 "description": "Maximum number of comments to include (0 or null for no comments)",
481 "minimum": 0,
482 "maximum": 100,
483 "default": None,
484 },
485 },
486 "required": ["issue_key"],
487 },
488 ),
489 Tool(
490 name="jira_search",
491 description="Search Jira issues using JQL (Jira Query Language)",
492 inputSchema={
493 "type": "object",
494 "properties": {
495 "jql": {
496 "type": "string",
497 "description": "JQL query string. Examples:\n"
498 '- Find Epics: "issuetype = Epic AND project = PROJ"\n'
499 '- Find issues in Epic: "parent = PROJ-123"\n'
500 "- Find by status: \"status = 'In Progress' AND project = PROJ\"\n"
501 '- Find by assignee: "assignee = currentUser()"\n'
502 '- Find recently updated: "updated >= -7d AND project = PROJ"\n'
503 '- Find by label: "labels = frontend AND project = PROJ"',
504 },
505 "fields": {
506 "type": "string",
507 "description": "Comma-separated fields to return",
508 "default": "*all",
509 },
510 "limit": {
511 "type": "number",
512 "description": "Maximum number of results (1-50)",
513 "default": 10,
514 "minimum": 1,
515 "maximum": 50,
516 },
517 },
518 "required": ["jql"],
519 },
520 ),
521 Tool(
522 name="jira_get_project_issues",
523 description="Get all issues for a specific Jira project",
524 inputSchema={
525 "type": "object",
526 "properties": {
527 "project_key": {
528 "type": "string",
529 "description": "The project key",
530 },
531 "limit": {
532 "type": "number",
533 "description": "Maximum number of results (1-50)",
534 "default": 10,
535 "minimum": 1,
536 "maximum": 50,
537 },
538 },
539 "required": ["project_key"],
540 },
541 ),
542 Tool(
543 name="jira_create_issue",
544 description="Create a new Jira issue with optional Epic link",
545 inputSchema={
546 "type": "object",
547 "properties": {
548 "project_key": {
549 "type": "string",
550 "description": "The JIRA project key (e.g. 'PROJ'). Never assume what it might be, always ask the user.",
551 },
552 "summary": {
553 "type": "string",
554 "description": "Summary/title of the issue",
555 },
556 "issue_type": {
557 "type": "string",
558 "description": "Issue type (e.g. 'Task', 'Bug', 'Story')",
559 },
560 "assignee": {
561 "type": "string",
562 "description": "Assignee of the ticket (accountID, full name or e-mail)",
563 },
564 "description": {
565 "type": "string",
566 "description": "Issue description",
567 "default": "",
568 },
569 "additional_fields": {
570 "type": "string",
571 "description": "Optional JSON string of additional fields to set. Examples:\n"
572 '- Link to Epic: {"parent": {"key": "PROJ-123"}} - For linking to an Epic after creation, prefer using the jira_link_to_epic tool instead\n'
573 '- Set priority: {"priority": {"name": "High"}} or {"priority": null} for no priority (common values: High, Medium, Low, None)\n'
574 '- Add labels: {"labels": ["label1", "label2"]}\n'
575 '- Set due date: {"duedate": "2023-12-31"}\n'
576 '- Custom fields: {"customfield_10XXX": "value"}',
577 "default": "{}",
578 },
579 },
580 "required": ["project_key", "summary", "issue_type"],
581 },
582 ),
583 Tool(
584 name="jira_update_issue",
585 description="Update an existing Jira issue including changing status, adding Epic links, updating fields, etc.",
586 inputSchema={
587 "type": "object",
588 "properties": {
589 "issue_key": {
590 "type": "string",
591 "description": "Jira issue key (e.g., 'PROJ-123')",
592 },
593 "fields": {
594 "type": "string",
595 "description": "A valid JSON object of fields to update. Examples:\n"
596 '- Add to Epic: {"parent": {"key": "PROJ-456"}} - Prefer using the dedicated jira_link_to_epic tool instead\n'
597 '- Change assignee: {"assignee": "user@email.com"} or {"assignee": null} to unassign\n'
598 '- Update summary: {"summary": "New title"}\n'
599 '- Update description: {"description": "New description"}\n'
600 "- Change status: requires transition IDs - use jira_get_transitions and jira_transition_issue instead\n"
601 '- Add labels: {"labels": ["label1", "label2"]}\n'
602 '- Set priority: {"priority": {"name": "High"}} or {"priority": null} for no priority (common values: High, Medium, Low, None)\n'
603 '- Update custom fields: {"customfield_10XXX": "value"}',
604 },
605 "additional_fields": {
606 "type": "string",
607 "description": "Optional JSON string of additional fields to update",
608 "default": "{}",
609 },
610 },
611 "required": ["issue_key", "fields"],
612 },
613 ),
614 Tool(
615 name="jira_delete_issue",
616 description="Delete an existing Jira issue",
617 inputSchema={
618 "type": "object",
619 "properties": {
620 "issue_key": {
621 "type": "string",
622 "description": "Jira issue key (e.g. PROJ-123)",
623 },
624 },
625 "required": ["issue_key"],
626 },
627 ),
628 Tool(
629 name="jira_add_comment",
630 description="Add a comment to a Jira issue",
631 inputSchema={
632 "type": "object",
633 "properties": {
634 "issue_key": {
635 "type": "string",
636 "description": "Jira issue key (e.g., 'PROJ-123')",
637 },
638 "comment": {
639 "type": "string",
640 "description": "Comment text in Markdown format",
641 },
642 },
643 "required": ["issue_key", "comment"],
644 },
645 ),
646 Tool(
647 name="jira_add_worklog",
648 description="Add a worklog entry to a Jira issue",
649 inputSchema={
650 "type": "object",
651 "properties": {
652 "issue_key": {
653 "type": "string",
654 "description": "Jira issue key (e.g., 'PROJ-123')",
655 },
656 "time_spent": {
657 "type": "string",
658 "description": "Time spent in Jira format (e.g., '1h 30m', '1d', '30m')",
659 },
660 "comment": {
661 "type": "string",
662 "description": "Optional comment for the worklog in Markdown format",
663 },
664 "started": {
665 "type": "string",
666 "description": "Optional start time in ISO format (e.g. '2023-08-01T12:00:00.000+0000'). If not provided, current time will be used.",
667 },
668 "original_estimate": {
669 "type": "string",
670 "description": "Optional original estimate in Jira format (e.g., '1h 30m', '1d'). This will update the original estimate for the issue.",
671 },
672 "remaining_estimate": {
673 "type": "string",
674 "description": "Optional remaining estimate in Jira format (e.g., '1h', '30m'). This will update the remaining estimate for the issue.",
675 },
676 },
677 "required": ["issue_key", "time_spent"],
678 },
679 ),
680 Tool(
681 name="jira_get_worklog",
682 description="Get worklog entries for a Jira issue",
683 inputSchema={
684 "type": "object",
685 "properties": {
686 "issue_key": {
687 "type": "string",
688 "description": "Jira issue key (e.g., 'PROJ-123')",
689 },
690 },
691 "required": ["issue_key"],
692 },
693 ),
694 Tool(
695 name="jira_link_to_epic",
696 description="Link an existing issue to an epic",
697 inputSchema={
698 "type": "object",
699 "properties": {
700 "issue_key": {
701 "type": "string",
702 "description": "The key of the issue to link (e.g., 'PROJ-123')",
703 },
704 "epic_key": {
705 "type": "string",
706 "description": "The key of the epic to link to (e.g., 'PROJ-456')",
707 },
708 },
709 "required": ["issue_key", "epic_key"],
710 },
711 ),
712 Tool(
713 name="jira_get_epic_issues",
714 description="Get all issues linked to a specific epic",
715 inputSchema={
716 "type": "object",
717 "properties": {
718 "epic_key": {
719 "type": "string",
720 "description": "The key of the epic (e.g., 'PROJ-123')",
721 },
722 "limit": {
723 "type": "number",
724 "description": "Maximum number of issues to return (1-50)",
725 "default": 10,
726 "minimum": 1,
727 "maximum": 50,
728 },
729 },
730 "required": ["epic_key"],
731 },
732 ),
733 Tool(
734 name="jira_get_transitions",
735 description="Get available status transitions for a Jira issue",
736 inputSchema={
737 "type": "object",
738 "properties": {
739 "issue_key": {
740 "type": "string",
741 "description": "Jira issue key (e.g., 'PROJ-123')",
742 },
743 },
744 "required": ["issue_key"],
745 },
746 ),
747 Tool(
748 name="jira_transition_issue",
749 description="Transition a Jira issue to a new status",
750 inputSchema={
751 "type": "object",
752 "properties": {
753 "issue_key": {
754 "type": "string",
755 "description": "Jira issue key (e.g., 'PROJ-123')",
756 },
757 "transition_id": {
758 "type": "string",
759 "description": "ID of the transition to perform (get this from jira_get_transitions)",
760 },
761 "fields": {
762 "type": "string",
763 "description": "JSON string of fields to update during the transition (optional)",
764 "default": "{}",
765 },
766 "comment": {
767 "type": "string",
768 "description": "Comment to add during the transition (optional)",
769 },
770 },
771 "required": ["issue_key", "transition_id"],
772 },
773 ),
774 ]
775 )
777 return tools
780@app.call_tool()
781async def call_tool(name: str, arguments: dict[str, Any]) -> Sequence[TextContent]:
782 """Handle tool calls for Confluence and Jira operations."""
783 try:
784 # Helper functions for formatting results
785 def format_comment(comment: dict) -> dict:
786 """
787 Format a Jira comment for display.
789 Args:
790 comment: The raw comment dictionary from Jira
792 Returns:
793 Formatted comment dictionary with selected fields
794 """
795 return {
796 "id": comment.get("id"),
797 "author": comment.get("author", {}).get("displayName", "Unknown"),
798 "created": comment.get("created"),
799 "body": comment.get("body"),
800 }
802 def format_issue(doc: Document) -> dict:
803 """
804 Format a Jira issue document for display.
806 Args:
807 doc: The Document object containing issue data
809 Returns:
810 Formatted issue dictionary with selected fields
811 """
812 return {
813 "key": doc.metadata.get("key", ""),
814 "title": doc.metadata.get("title", ""),
815 "type": doc.metadata.get("type", "Unknown"),
816 "status": doc.metadata.get("status", "Unknown"),
817 "created_date": doc.metadata.get("created_date", ""),
818 "priority": doc.metadata.get("priority", "None"),
819 "link": doc.metadata.get("link", ""),
820 }
822 def format_transition(transition: dict) -> dict:
823 """
824 Format a Jira transition for display.
826 Args:
827 transition: The raw transition dictionary from Jira
829 Returns:
830 Formatted transition dictionary with selected fields
831 """
832 return {
833 "id": transition.get("id"),
834 "name": transition.get("name"),
835 "to_status": transition.get("to", {}).get("name"),
836 }
838 # Dispatch to the appropriate handler based on the tool name
839 tool_handlers = {
840 # Confluence tools
841 "confluence_search": handle_confluence_search,
842 "confluence_get_page": handle_confluence_get_page,
843 "confluence_get_comments": handle_confluence_get_comments,
844 "confluence_create_page": handle_confluence_create_page,
845 "confluence_update_page": handle_confluence_update_page,
846 # Jira tools
847 "jira_get_issue": handle_jira_get_issue,
848 "jira_search": handle_jira_search,
849 "jira_get_project_issues": handle_jira_get_project_issues,
850 "jira_create_issue": handle_jira_create_issue,
851 "jira_update_issue": handle_jira_update_issue,
852 "jira_delete_issue": handle_jira_delete_issue,
853 "jira_add_comment": handle_jira_add_comment,
854 "jira_add_worklog": handle_jira_add_worklog,
855 "jira_get_worklog": handle_jira_get_worklog,
856 "jira_get_transitions": handle_jira_get_transitions,
857 "jira_transition_issue": handle_jira_transition_issue,
858 "jira_link_to_epic": handle_jira_link_to_epic,
859 "jira_get_epic_issues": handle_jira_get_epic_issues,
860 }
862 if name in tool_handlers:
863 return tool_handlers[name](
864 arguments, format_comment, format_issue, format_transition
865 )
866 else:
867 error_msg = f"Unsupported tool: {name}"
868 logger.error(error_msg)
869 return [TextContent(type="error", text=error_msg)]
871 except KeyError as e:
872 error_msg = f"Missing required parameter for tool {name}: {str(e)}"
873 logger.error(error_msg)
874 return [TextContent(type="error", text=error_msg)]
875 except (ValueError, TypeError) as e:
876 error_msg = f"Invalid parameter value for tool {name}: {str(e)}"
877 logger.error(error_msg)
878 return [TextContent(type="error", text=error_msg)]
879 except AttributeError as e:
880 error_msg = f"Tool execution error for {name}: {str(e)}"
881 logger.error(error_msg)
882 return [TextContent(type="error", text=error_msg)]
883 except Exception as e: # noqa: BLE001 - Intentional fallback with logging
884 error_msg = f"Unexpected error executing tool {name}: {str(e)}"
885 logger.error(error_msg)
886 logger.debug(f"Full exception details for tool {name}:", exc_info=True)
887 return [TextContent(type="error", text=error_msg)]
890def handle_confluence_search(
891 arguments: dict[str, Any],
892 format_comment: CommentFormatter,
893 format_issue: IssueFormatter,
894 format_transition: TransitionFormatter,
895) -> Sequence[TextContent]:
896 """
897 Handle confluence_search tool.
899 Args:
900 arguments: The tool arguments
901 format_comment: Helper function for formatting comments
902 format_issue: Helper function for formatting issues
903 format_transition: Helper function for formatting transitions
905 Returns:
906 Search results
907 """
908 # Ensure Confluence is configured
909 if confluence_fetcher is None:
910 return [
911 TextContent(
912 text="Confluence is not configured. Please set CONFLUENCE_URL, CONFLUENCE_USERNAME, and CONFLUENCE_API_TOKEN environment variables.",
913 type="text",
914 )
915 ]
917 query = arguments["query"]
918 limit = arguments.get("limit", 10)
920 try:
921 results = confluence_fetcher.search(query, limit=limit)
922 return [TextContent(type="text", text=results)]
923 except KeyError as e:
924 error_msg = f"Missing key in search parameters or results: {str(e)}"
925 logger.error(error_msg)
926 return [TextContent(type="text", text=error_msg)]
927 except ValueError as e:
928 error_msg = f"Invalid search parameter: {str(e)}"
929 logger.error(error_msg)
930 return [TextContent(type="text", text=error_msg)]
931 except Exception as e: # noqa: BLE001 - Intentional fallback with logging
932 error_msg = f"Unexpected error searching Confluence: {str(e)}"
933 logger.error(error_msg)
934 logger.debug("Full exception details for Confluence search:", exc_info=True)
935 return [TextContent(type="text", text=error_msg)]
938def handle_confluence_get_page(
939 arguments: dict[str, Any],
940 format_comment: CommentFormatter,
941 format_issue: IssueFormatter,
942 format_transition: TransitionFormatter,
943) -> Sequence[TextContent]:
944 """
945 Handle confluence_get_page tool.
947 Args:
948 arguments: The tool arguments
949 format_comment: Helper function for formatting comments
950 format_issue: Helper function for formatting issues
951 format_transition: Helper function for formatting transitions
953 Returns:
954 Formatted page content
955 """
956 doc = confluence_fetcher.get_page_content(arguments["page_id"])
957 include_metadata = arguments.get("include_metadata", True)
959 if include_metadata:
960 result = {"content": doc.page_content, "metadata": doc.metadata}
961 else:
962 result = {"content": doc.page_content}
964 return [
965 TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))
966 ]
969def handle_confluence_get_comments(
970 arguments: dict[str, Any],
971 format_comment: CommentFormatter,
972 format_issue: IssueFormatter,
973 format_transition: TransitionFormatter,
974) -> Sequence[TextContent]:
975 """
976 Handle confluence_get_comments tool.
978 Args:
979 arguments: The tool arguments
980 format_comment: Helper function for formatting comments
981 format_issue: Helper function for formatting issues
982 format_transition: Helper function for formatting transitions
984 Returns:
985 Formatted comments
986 """
987 comments = confluence_fetcher.get_page_comments(
988 page_id=arguments["page_id"], return_markdown=True
989 )
990 # Convert Document objects to dictionaries for the formatter
991 formatted_comments = [format_comment(doc.metadata) for doc in comments]
993 return [
994 TextContent(
995 type="text",
996 text=json.dumps(formatted_comments, indent=2, ensure_ascii=False),
997 )
998 ]
1001def handle_confluence_create_page(
1002 arguments: dict[str, Any],
1003 format_comment: CommentFormatter,
1004 format_issue: IssueFormatter,
1005 format_transition: TransitionFormatter,
1006) -> Sequence[TextContent]:
1007 """
1008 Handle confluence_create_page tool.
1010 Args:
1011 arguments: The tool arguments
1012 format_comment: Helper function for formatting comments
1013 format_issue: Helper function for formatting issues
1014 format_transition: Helper function for formatting transitions
1016 Returns:
1017 Formatted page creation result
1018 """
1019 # Convert markdown content to HTML storage format
1020 space_key = arguments["space_key"]
1021 title = arguments["title"]
1022 content = arguments["content"]
1023 parent_id = arguments.get("parent_id")
1025 # Convert markdown to Confluence storage format
1026 storage_format = markdown_to_confluence_storage(content)
1028 # Handle parent_id - convert to string if not None
1029 parent_id_str: str | None = str(parent_id) if parent_id is not None else None
1031 # Create the page
1032 doc = confluence_fetcher.create_page(
1033 space_key=space_key,
1034 title=title,
1035 body=storage_format, # Now using the converted storage format
1036 parent_id=parent_id_str,
1037 )
1039 result = {
1040 "page_id": doc.metadata["page_id"],
1041 "title": doc.metadata["title"],
1042 "space_key": doc.metadata["space_key"],
1043 "url": doc.metadata["url"],
1044 "version": doc.metadata["version"],
1045 "content": doc.page_content[:500] + "..."
1046 if len(doc.page_content) > 500
1047 else doc.page_content,
1048 }
1050 return [
1051 TextContent(
1052 type="text",
1053 text=f"Page created successfully:\n{json.dumps(result, indent=2, ensure_ascii=False)}",
1054 )
1055 ]
1058def handle_confluence_update_page(
1059 arguments: dict[str, Any],
1060 format_comment: CommentFormatter,
1061 format_issue: IssueFormatter,
1062 format_transition: TransitionFormatter,
1063) -> Sequence[TextContent]:
1064 """
1065 Handle confluence_update_page tool.
1067 Args:
1068 arguments: The tool arguments
1069 format_comment: Helper function for formatting comments
1070 format_issue: Helper function for formatting issues
1071 format_transition: Helper function for formatting transitions
1073 Returns:
1074 Formatted page update result
1075 """
1076 page_id = arguments["page_id"]
1077 title = arguments["title"]
1078 content = arguments["content"]
1079 minor_edit = arguments.get("minor_edit", False)
1080 version_comment = arguments.get("version_comment", "")
1082 # Convert markdown to Confluence storage format
1083 storage_format = markdown_to_confluence_storage(content)
1085 # Update the page
1086 doc = confluence_fetcher.update_page(
1087 page_id=page_id,
1088 title=title,
1089 body=storage_format,
1090 is_minor_edit=minor_edit,
1091 version_comment=version_comment,
1092 )
1094 result = {
1095 "page_id": doc.metadata["page_id"],
1096 "title": doc.metadata["title"],
1097 "space_key": doc.metadata["space_key"],
1098 "url": doc.metadata["url"],
1099 "version": doc.metadata["version"],
1100 "content": doc.page_content[:500] + "..."
1101 if len(doc.page_content) > 500
1102 else doc.page_content,
1103 }
1105 return [
1106 TextContent(
1107 type="text",
1108 text=f"Page updated successfully:\n{json.dumps(result, indent=2, ensure_ascii=False)}",
1109 )
1110 ]
1113def handle_jira_get_issue(
1114 arguments: dict[str, Any],
1115 format_comment: CommentFormatter,
1116 format_issue: IssueFormatter,
1117 format_transition: TransitionFormatter,
1118) -> Sequence[TextContent]:
1119 """
1120 Handle jira_get_issue tool.
1122 Args:
1123 arguments: The tool arguments
1124 format_comment: Helper function for formatting comments
1125 format_issue: Helper function for formatting issues
1126 format_transition: Helper function for formatting transitions
1128 Returns:
1129 Issue details
1130 """
1131 # Ensure Jira is configured
1132 if jira_fetcher is None:
1133 return [
1134 TextContent(
1135 text="Jira is not configured. Please set JIRA_URL, JIRA_USERNAME, and JIRA_API_TOKEN environment variables.",
1136 type="text",
1137 )
1138 ]
1140 issue_key = arguments["issue_key"]
1141 expand = arguments.get("expand")
1142 comment_limit = arguments.get("comment_limit", 10)
1144 try:
1145 doc = jira_fetcher.get_issue(
1146 issue_key, expand=expand, comment_limit=comment_limit
1147 )
1148 return [TextContent(type="text", text=format_issue(doc))]
1149 except KeyError as e:
1150 error_msg = f"Missing key in Jira issue {issue_key}: {str(e)}"
1151 logger.error(error_msg)
1152 return [TextContent(type="text", text=error_msg)]
1153 except ValueError as e:
1154 error_msg = f"Invalid parameter for Jira issue {issue_key}: {str(e)}"
1155 logger.error(error_msg)
1156 return [TextContent(type="text", text=error_msg)]
1157 except Exception as e: # noqa: BLE001 - Intentional fallback with logging
1158 error_msg = f"Unexpected error getting Jira issue {issue_key}: {str(e)}"
1159 logger.error(error_msg)
1160 logger.debug(
1161 f"Full exception details for Jira issue {issue_key}:", exc_info=True
1162 )
1163 return [TextContent(type="text", text=error_msg)]
1166def handle_jira_search(
1167 arguments: dict[str, Any],
1168 format_comment: CommentFormatter,
1169 format_issue: IssueFormatter,
1170 format_transition: TransitionFormatter,
1171) -> Sequence[TextContent]:
1172 """
1173 Handle jira_search tool.
1175 Args:
1176 arguments: The tool arguments
1177 format_comment: Helper function for formatting comments
1178 format_issue: Helper function for formatting issues
1179 format_transition: Helper function for formatting transitions
1181 Returns:
1182 Formatted search results
1183 """
1184 jql = arguments["query"]
1185 limit = min(int(arguments.get("limit", 10)), 50)
1186 start = int(arguments.get("start", 0))
1188 documents = jira_fetcher.search_issues(jql=jql, limit=limit, start=start)
1189 results = [format_issue(doc) for doc in documents]
1191 return [
1192 TextContent(type="text", text=json.dumps(results, indent=2, ensure_ascii=False))
1193 ]
1196def handle_jira_get_project_issues(
1197 arguments: dict[str, Any],
1198 format_comment: CommentFormatter,
1199 format_issue: IssueFormatter,
1200 format_transition: TransitionFormatter,
1201) -> Sequence[TextContent]:
1202 """
1203 Handle jira_get_project_issues tool.
1205 Args:
1206 arguments: The tool arguments
1207 format_comment: Helper function for formatting comments
1208 format_issue: Helper function for formatting issues
1209 format_transition: Helper function for formatting transitions
1211 Returns:
1212 Formatted project issues
1213 """
1214 project_key = arguments["project_key"]
1215 limit = min(int(arguments.get("limit", 10)), 50)
1216 start = int(arguments.get("start", 0))
1218 documents = jira_fetcher.get_project_issues(
1219 project_key=project_key, limit=limit, start=start
1220 )
1221 results = [format_issue(doc) for doc in documents]
1223 return [
1224 TextContent(type="text", text=json.dumps(results, indent=2, ensure_ascii=False))
1225 ]
1228def handle_jira_create_issue(
1229 arguments: dict[str, Any],
1230 format_comment: CommentFormatter,
1231 format_issue: IssueFormatter,
1232 format_transition: TransitionFormatter,
1233) -> Sequence[TextContent]:
1234 """
1235 Handle jira_create_issue tool.
1237 Args:
1238 arguments: The tool arguments
1239 format_comment: Helper function for formatting comments
1240 format_issue: Helper function for formatting issues
1241 format_transition: Helper function for formatting transitions
1243 Returns:
1244 Formatted issue creation result
1245 """
1246 # Extract required arguments
1247 project_key = arguments["project_key"]
1248 summary = arguments["summary"]
1249 issue_type = arguments["issue_type"]
1251 # Extract optional arguments
1252 description = arguments.get("description", "")
1253 assignee = arguments.get("assignee")
1255 # Create a shallow copy of arguments without the standard fields
1256 # to pass any remaining ones as custom fields
1257 custom_fields = arguments.copy()
1258 for field in ["project_key", "summary", "issue_type", "description", "assignee"]:
1259 custom_fields.pop(field, None)
1261 # Create the issue
1262 doc = jira_fetcher.create_issue(
1263 project_key=project_key,
1264 summary=summary,
1265 issue_type=issue_type,
1266 description=description,
1267 assignee=assignee,
1268 **custom_fields,
1269 )
1271 result = format_issue(doc)
1272 result["description"] = doc.page_content
1274 return [
1275 TextContent(
1276 type="text",
1277 text=f"Issue created successfully:\n{json.dumps(result, indent=2, ensure_ascii=False)}",
1278 )
1279 ]
1282def handle_jira_update_issue(
1283 arguments: dict[str, Any],
1284 format_comment: CommentFormatter,
1285 format_issue: IssueFormatter,
1286 format_transition: TransitionFormatter,
1287) -> Sequence[TextContent]:
1288 """
1289 Handle jira_update_issue tool.
1291 Args:
1292 arguments: The tool arguments
1293 format_comment: Helper function for formatting comments
1294 format_issue: Helper function for formatting issues
1295 format_transition: Helper function for formatting transitions
1297 Returns:
1298 Formatted issue update result
1299 """
1300 # Extract issue key
1301 issue_key = arguments["issue_key"]
1303 # Create a shallow copy of arguments without the issue_key
1304 fields = arguments.copy()
1305 fields.pop("issue_key", None)
1307 # Update the issue
1308 doc = jira_fetcher.update_issue(issue_key=issue_key, **fields)
1310 result = format_issue(doc)
1311 result["description"] = doc.page_content
1313 return [
1314 TextContent(
1315 type="text",
1316 text=f"Issue updated successfully:\n{json.dumps(result, indent=2, ensure_ascii=False)}",
1317 )
1318 ]
1321def handle_jira_delete_issue(
1322 arguments: dict[str, Any],
1323 format_comment: CommentFormatter,
1324 format_issue: IssueFormatter,
1325 format_transition: TransitionFormatter,
1326) -> Sequence[TextContent]:
1327 """
1328 Handle jira_delete_issue tool.
1330 Args:
1331 arguments: The tool arguments
1332 format_comment: Helper function for formatting comments
1333 format_issue: Helper function for formatting issues
1334 format_transition: Helper function for formatting transitions
1336 Returns:
1337 Deletion confirmation
1338 """
1339 issue_key = arguments["issue_key"]
1340 success = jira_fetcher.delete_issue(issue_key)
1342 if success:
1343 return [
1344 TextContent(
1345 type="text",
1346 text=f"Issue {issue_key} deleted successfully.",
1347 )
1348 ]
1349 else:
1350 return [
1351 TextContent(
1352 type="error",
1353 text=f"Failed to delete issue {issue_key}.",
1354 )
1355 ]
1358def handle_jira_add_comment(
1359 arguments: dict[str, Any],
1360 format_comment: CommentFormatter,
1361 format_issue: IssueFormatter,
1362 format_transition: TransitionFormatter,
1363) -> Sequence[TextContent]:
1364 """
1365 Handle jira_add_comment tool.
1367 Args:
1368 arguments: The tool arguments
1369 format_comment: Helper function for formatting comments
1370 format_issue: Helper function for formatting issues
1371 format_transition: Helper function for formatting transitions
1373 Returns:
1374 Comment addition confirmation
1375 """
1376 issue_key = arguments["issue_key"]
1377 comment_text = arguments["comment"]
1379 result = jira_fetcher.add_comment(issue_key, comment_text)
1380 formatted_result = format_comment(result)
1382 return [
1383 TextContent(
1384 type="text",
1385 text=f"Comment added successfully:\n{json.dumps(formatted_result, indent=2, ensure_ascii=False)}",
1386 )
1387 ]
1390def handle_jira_add_worklog(
1391 arguments: dict[str, Any],
1392 format_comment: CommentFormatter,
1393 format_issue: IssueFormatter,
1394 format_transition: TransitionFormatter,
1395) -> Sequence[TextContent]:
1396 """
1397 Handle jira_add_worklog tool.
1399 Args:
1400 arguments: The tool arguments
1401 format_comment: Helper function for formatting comments
1402 format_issue: Helper function for formatting issues
1403 format_transition: Helper function for formatting transitions
1405 Returns:
1406 Worklog addition confirmation
1407 """
1408 # Ensure Jira is configured
1409 if jira_fetcher is None:
1410 return [
1411 TextContent(
1412 text="Jira is not configured. Please set JIRA_URL, JIRA_USERNAME, and JIRA_API_TOKEN environment variables.",
1413 type="text",
1414 )
1415 ]
1417 issue_key = arguments["issue_key"]
1418 time_spent = arguments["time_spent"]
1420 # Process optional parameters with proper type checking
1421 comment = arguments.get("comment")
1422 if comment is not None and not isinstance(comment, str):
1423 comment = str(comment)
1425 started = arguments.get("started")
1426 if started is not None and not isinstance(started, str):
1427 started = str(started)
1429 original_estimate = arguments.get("original_estimate")
1430 if original_estimate is not None and not isinstance(original_estimate, str):
1431 original_estimate = str(original_estimate)
1433 remaining_estimate = arguments.get("remaining_estimate")
1434 if remaining_estimate is not None and not isinstance(remaining_estimate, str):
1435 remaining_estimate = str(remaining_estimate)
1437 try:
1438 result = jira_fetcher.add_worklog(
1439 issue_key=issue_key,
1440 time_spent=time_spent,
1441 comment=comment,
1442 started=started,
1443 original_estimate=original_estimate,
1444 remaining_estimate=remaining_estimate,
1445 )
1447 return [
1448 TextContent(
1449 type="text",
1450 text=f"Worklog added successfully:\n{json.dumps(result, indent=2, ensure_ascii=False)}",
1451 )
1452 ]
1453 except KeyError as e:
1454 error_msg = f"Missing required field for worklog on {issue_key}: {str(e)}"
1455 logger.error(error_msg)
1456 return [TextContent(type="text", text=error_msg)]
1457 except ValueError as e:
1458 error_msg = f"Invalid value for worklog parameter on {issue_key}: {str(e)}"
1459 logger.error(error_msg)
1460 return [TextContent(type="text", text=error_msg)]
1461 except Exception as e: # noqa: BLE001 - Intentional fallback with logging
1462 error_msg = f"Unexpected error adding worklog to {issue_key}: {str(e)}"
1463 logger.error(error_msg)
1464 logger.debug(
1465 f"Full exception details for worklog on {issue_key}:", exc_info=True
1466 )
1467 return [TextContent(type="text", text=error_msg)]
1470def handle_jira_get_worklog(
1471 arguments: dict[str, Any],
1472 format_comment: CommentFormatter,
1473 format_issue: IssueFormatter,
1474 format_transition: TransitionFormatter,
1475) -> Sequence[TextContent]:
1476 """
1477 Handle jira_get_worklog tool.
1479 Args:
1480 arguments: The tool arguments
1481 format_comment: Helper function for formatting comments
1482 format_issue: Helper function for formatting issues
1483 format_transition: Helper function for formatting transitions
1485 Returns:
1486 Formatted worklog entries
1487 """
1488 issue_key = arguments["issue_key"]
1489 worklogs = jira_fetcher.get_worklogs(issue_key)
1491 return [
1492 TextContent(
1493 type="text",
1494 text=json.dumps(worklogs, indent=2, ensure_ascii=False),
1495 )
1496 ]
1499def handle_jira_get_transitions(
1500 arguments: dict[str, Any],
1501 format_comment: CommentFormatter,
1502 format_issue: IssueFormatter,
1503 format_transition: TransitionFormatter,
1504) -> Sequence[TextContent]:
1505 """
1506 Handle jira_get_transitions tool.
1508 Args:
1509 arguments: The tool arguments
1510 format_comment: Helper function for formatting comments
1511 format_issue: Helper function for formatting issues
1512 format_transition: Helper function for formatting transitions
1514 Returns:
1515 Formatted transition options
1516 """
1517 issue_key = arguments["issue_key"]
1518 transitions = jira_fetcher.get_available_transitions(issue_key)
1519 formatted_transitions = [format_transition(t) for t in transitions]
1521 return [
1522 TextContent(
1523 type="text",
1524 text=json.dumps(formatted_transitions, indent=2, ensure_ascii=False),
1525 )
1526 ]
1529def handle_jira_transition_issue(
1530 arguments: dict[str, Any],
1531 format_comment: CommentFormatter,
1532 format_issue: IssueFormatter,
1533 format_transition: TransitionFormatter,
1534) -> Sequence[TextContent]:
1535 """
1536 Handle jira_transition_issue tool.
1538 Args:
1539 arguments: The tool arguments
1540 format_comment: Helper function for formatting comments
1541 format_issue: Helper function for formatting issues
1542 format_transition: Helper function for formatting transitions
1544 Returns:
1545 Transition confirmation
1546 """
1547 issue_key = arguments["issue_key"]
1548 transition_id = arguments["transition_id"]
1550 # Optional arguments
1551 fields = arguments.get("fields")
1552 comment = arguments.get("comment")
1554 # Transition the issue
1555 doc = jira_fetcher.transition_issue(
1556 issue_key=issue_key,
1557 transition_id=transition_id,
1558 fields=fields,
1559 comment=comment,
1560 )
1562 result = format_issue(doc)
1563 result["description"] = doc.page_content
1565 return [
1566 TextContent(
1567 type="text",
1568 text=f"Issue transitioned successfully:\n{json.dumps(result, indent=2, ensure_ascii=False)}",
1569 )
1570 ]
1573def handle_jira_link_to_epic(
1574 arguments: dict[str, Any],
1575 format_comment: CommentFormatter,
1576 format_issue: IssueFormatter,
1577 format_transition: TransitionFormatter,
1578) -> Sequence[TextContent]:
1579 """
1580 Handle jira_link_to_epic tool.
1582 Args:
1583 arguments: The tool arguments
1584 format_comment: Helper function for formatting comments
1585 format_issue: Helper function for formatting issues
1586 format_transition: Helper function for formatting transitions
1588 Returns:
1589 Link confirmation
1590 """
1591 issue_key = arguments["issue_key"]
1592 epic_key = arguments["epic_key"]
1594 doc = jira_fetcher.link_issue_to_epic(issue_key, epic_key)
1595 result = format_issue(doc)
1596 result["description"] = doc.page_content
1597 result["epic_key"] = epic_key
1599 return [
1600 TextContent(
1601 type="text",
1602 text=f"Issue linked to epic successfully:\n{json.dumps(result, indent=2, ensure_ascii=False)}",
1603 )
1604 ]
1607def handle_jira_get_epic_issues(
1608 arguments: dict[str, Any],
1609 format_comment: CommentFormatter,
1610 format_issue: IssueFormatter,
1611 format_transition: TransitionFormatter,
1612) -> Sequence[TextContent]:
1613 """
1614 Handle jira_get_epic_issues tool.
1616 Args:
1617 arguments: The tool arguments
1618 format_comment: Helper function for formatting comments
1619 format_issue: Helper function for formatting issues
1620 format_transition: Helper function for formatting transitions
1622 Returns:
1623 Formatted epic issues
1624 """
1625 epic_key = arguments["epic_key"]
1626 limit = min(int(arguments.get("limit", 50)), 100)
1628 documents = jira_fetcher.get_epic_issues(epic_key, limit)
1629 results = [format_issue(doc) for doc in documents]
1631 return [
1632 TextContent(
1633 type="text",
1634 text=json.dumps(results, indent=2, ensure_ascii=False),
1635 )
1636 ]
1639async def main() -> None:
1640 """
1641 Run the MCP server in stdio mode.
1643 This function creates and runs the MCP server using the stdio interface,
1644 which enables communication with the MCP client through standard input/output.
1646 Returns:
1647 None
1648 """
1649 # Import here to avoid issues with event loops
1650 from mcp.server.stdio import stdio_server
1652 try:
1653 # Log the startup information
1654 logger.info("Starting MCP Atlassian server")
1655 if confluence_fetcher:
1656 logger.info(f"Confluence URL: {confluence_fetcher.config.url}")
1657 if jira_fetcher:
1658 logger.info(f"Jira URL: {jira_fetcher.config.url}")
1660 async with stdio_server() as (read_stream, write_stream):
1661 await app.run(
1662 read_stream, write_stream, app.create_initialization_options()
1663 )
1664 except Exception as err:
1665 logger.error(f"Error running server: {err}")
1666 error_msg = f"Failed to run server: {err}"
1667 raise RuntimeError(error_msg) from err
1670if __name__ == "__main__":
1671 import asyncio
1673 asyncio.run(main())