Repository: sooperset/mcp-atlassian

Issue:
[Feature]: Support wiki markup to storage format conversion in `confluence_create_page`
### Is your feature request related to a problem? Please describe.

Hi, I'm trying to use the `confluence_create_page` function from `mcp-atlassian` to create a page in the Confluence wiki using a specific template.

The template includes Confluence wiki markup content such as `{toc}` and `{change-history}`.
These macros work correctly when used directly in Confluence, but it seems that mcp-atlassian does not automatically convert them into the appropriate storage format when creating a page.

If there is a way to enable this conversion or if support is planned, I’d appreciate any guidance.

Reference:[https://confluence.atlassian.com/doc/confluence-wiki-markup-251003035.html](https://confluence.atlassian.com/doc/confluence-wiki-markup-251003035.html)

### Describe the solution you'd like

I'm not sure how the feature should be implemented internally, but I can provide the content using Confluence wiki markup.
It would be great if the mcp-atlassian server could automatically convert that wiki markup to the appropriate storage format when using the `confluence_create_page` tool.

### Describe alternatives you've considered

_No response_

### Use Case

The confluence_create_page tool in mcp-atlassian accepts content like the example below, written in Confluence wiki markup (e.g., `{toc}`, `{panel}`),
but it does not automatically convert it into the required storage format.
As a result, the raw markup appears as plain text on the page instead of being rendered properly.

```
{panel:borderStyle=dashed} Change History

{change-history:limit=3} {panel}

{panel:borderWidth=2} Table of Contents

{toc:maxLevel=3} {panel}

h1. Summary

{info} Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam id dolor id nibh ultricies vehicula ut id elit. Sed posuere consectetur est at lobortis. {info}

h1. Overview

{info} * Purpose: Lorem ipsum dolor sit amet, consectetur adipiscing elit.  
* Scope: Curabitur blandit tempus porttitor. Nulla vitae elit libero, a pharetra augue.  
* Intended Audience: Lorem ipsum, DevOps lorem, API documentation lorem, and project lorem. {info}

h1. Background

{info} * Current Situation: Lorem ipsum dolor sit amet, consectetur adipiscing elit.  
* Pain Points:  
** Lorem ipsum dolor sit amet  
** Sed posuere consectetur est  
** Cras justo odio, dapibus ac facilisis in  
* Need for Improvement: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. {info}
```

### Additional Context

_No response_


Discussion:
Hi @kakao-edgar-p, this is a great feature request and definitely doable! The underlying API already supports wiki markup conversion. I'll work on adding a `content_format` parameter to `confluence_create_page` so you can specify "wiki", "markdown", or "storage" format.

Will implement this soon 👍
Hi, I've implemented this feature on PR #531 and merged. I'll release the new version soon!



Patch:
diff --git a/src/mcp_atlassian/confluence/pages.py b/src/mcp_atlassian/confluence/pages.py
index 49dcf925..e4554427 100644
--- a/src/mcp_atlassian/confluence/pages.py
+++ b/src/mcp_atlassian/confluence/pages.py
@@ -277,6 +277,7 @@ def create_page(
         *,
         is_markdown: bool = True,
         enable_heading_anchors: bool = False,
+        content_representation: str | None = None,
     ) -> ConfluencePage:
         """
         Create a new page in a Confluence space.
@@ -284,10 +285,11 @@ def create_page(
         Args:
             space_key: The key of the space to create the page in
             title: The title of the new page
-            body: The content of the page (markdown or storage format)
+            body: The content of the page (markdown, wiki markup, or storage format)
             parent_id: Optional ID of a parent page
             is_markdown: Whether the body content is in markdown format (default: True, keyword-only)
             enable_heading_anchors: Whether to enable automatic heading anchor generation (default: False, keyword-only)
+            content_representation: Content format when is_markdown=False ('wiki' or 'storage', keyword-only)
 
         Returns:
             ConfluencePage model containing the new page's data
@@ -296,14 +298,17 @@ def create_page(
             Exception: If there is an error creating the page
         """
         try:
-            # Convert markdown to Confluence storage format if needed
-            storage_body = (
-                self.preprocessor.markdown_to_confluence_storage(
+            # Determine body and representation based on content type
+            if is_markdown:
+                # Convert markdown to Confluence storage format
+                final_body = self.preprocessor.markdown_to_confluence_storage(
                     body, enable_heading_anchors=enable_heading_anchors
                 )
-                if is_markdown
-                else body
-            )
+                representation = "storage"
+            else:
+                # Use body as-is with specified representation
+                final_body = body
+                representation = content_representation or "storage"
 
             # Use v2 API for OAuth authentication, v1 API for token/basic auth
             v2_adapter = self._v2_adapter
@@ -314,9 +319,9 @@ def create_page(
                 result = v2_adapter.create_page(
                     space_key=space_key,
                     title=title,
-                    body=storage_body,
+                    body=final_body,
                     parent_id=parent_id,
-                    representation="storage",
+                    representation=representation,
                 )
             else:
                 logger.debug(
@@ -325,9 +330,9 @@ def create_page(
                 result = self.confluence.create_page(
                     space=space_key,
                     title=title,
-                    body=storage_body,
+                    body=final_body,
                     parent_id=parent_id,
-                    representation="storage",
+                    representation=representation,
                 )
 
             # Get the new page content
@@ -355,6 +360,7 @@ def update_page(
         is_markdown: bool = True,
         parent_id: str | None = None,
         enable_heading_anchors: bool = False,
+        content_representation: str | None = None,
     ) -> ConfluencePage:
         """
         Update an existing page in Confluence.
@@ -362,12 +368,13 @@ def update_page(
         Args:
             page_id: The ID of the page to update
             title: The new title of the page
-            body: The new content of the page (markdown or storage format)
+            body: The new content of the page (markdown, wiki markup, or storage format)
             is_minor_edit: Whether this is a minor edit (keyword-only)
             version_comment: Optional comment for this version (keyword-only)
             is_markdown: Whether the body content is in markdown format (default: True, keyword-only)
             parent_id: Optional new parent page ID (keyword-only)
             enable_heading_anchors: Whether to enable automatic heading anchor generation (default: False, keyword-only)
+            content_representation: Content format when is_markdown=False ('wiki' or 'storage', keyword-only)
 
         Returns:
             ConfluencePage model containing the updated page's data
@@ -376,14 +383,17 @@ def update_page(
             Exception: If there is an error updating the page
         """
         try:
-            # Convert markdown to Confluence storage format if needed
-            storage_body = (
-                self.preprocessor.markdown_to_confluence_storage(
+            # Determine body and representation based on content type
+            if is_markdown:
+                # Convert markdown to Confluence storage format
+                final_body = self.preprocessor.markdown_to_confluence_storage(
                     body, enable_heading_anchors=enable_heading_anchors
                 )
-                if is_markdown
-                else body
-            )
+                representation = "storage"
+            else:
+                # Use body as-is with specified representation
+                final_body = body
+                representation = content_representation or "storage"
 
             logger.debug(f"Updating page {page_id} with title '{title}'")
 
@@ -396,8 +406,8 @@ def update_page(
                 response = v2_adapter.update_page(
                     page_id=page_id,
                     title=title,
-                    body=storage_body,
-                    representation="storage",
+                    body=final_body,
+                    representation=representation,
                     version_comment=version_comment,
                 )
             else:
@@ -407,9 +417,9 @@ def update_page(
                 update_kwargs = {
                     "page_id": page_id,
                     "title": title,
-                    "body": storage_body,
+                    "body": final_body,
                     "type": "page",
-                    "representation": "storage",
+                    "representation": representation,
                     "minor_edit": is_minor_edit,
                     "version_comment": version_comment,
                     "always_update": True,
diff --git a/src/mcp_atlassian/servers/confluence.py b/src/mcp_atlassian/servers/confluence.py
index 622e5a3c..686cc059 100644
--- a/src/mcp_atlassian/servers/confluence.py
+++ b/src/mcp_atlassian/servers/confluence.py
@@ -415,7 +415,7 @@ async def create_page(
     content: Annotated[
         str,
         Field(
-            description="The content of the page in Markdown format. Supports headings, lists, tables, code blocks, and other Markdown syntax"
+            description="The content of the page. Format depends on content_format parameter. Can be Markdown (default), wiki markup, or storage format"
         ),
     ],
     parent_id: Annotated[
@@ -425,6 +425,20 @@ async def create_page(
             default="",
         ),
     ] = "",
+    content_format: Annotated[
+        str,
+        Field(
+            description="(Optional) The format of the content parameter. Options: 'markdown' (default), 'wiki', or 'storage'. Wiki format uses Confluence wiki markup syntax",
+            default="markdown",
+        ),
+    ] = "markdown",
+    enable_heading_anchors: Annotated[
+        bool,
+        Field(
+            description="(Optional) Whether to enable automatic heading anchor generation. Only applies when content_format is 'markdown'",
+            default=False,
+        ),
+    ] = False,
 ) -> str:
     """Create a new Confluence page.
 
@@ -432,22 +446,43 @@ async def create_page(
         ctx: The FastMCP context.
         space_key: The key of the space.
         title: The title of the page.
-        content: The content in Markdown format.
+        content: The content of the page (format depends on content_format).
         parent_id: Optional parent page ID.
+        content_format: The format of the content ('markdown', 'wiki', or 'storage').
+        enable_heading_anchors: Whether to enable heading anchors (markdown only).
 
     Returns:
         JSON string representing the created page object.
 
     Raises:
-        ValueError: If in read-only mode or Confluence client is unavailable.
+        ValueError: If in read-only mode, Confluence client is unavailable, or invalid content_format.
     """
     confluence_fetcher = await get_confluence_fetcher(ctx)
+
+    # Validate content_format
+    if content_format not in ["markdown", "wiki", "storage"]:
+        raise ValueError(
+            f"Invalid content_format: {content_format}. Must be 'markdown', 'wiki', or 'storage'"
+        )
+
+    # Determine parameters based on content format
+    if content_format == "markdown":
+        is_markdown = True
+        content_representation = None  # Will be converted to storage
+    else:
+        is_markdown = False
+        content_representation = content_format  # Pass 'wiki' or 'storage' directly
+
     page = confluence_fetcher.create_page(
         space_key=space_key,
         title=title,
         body=content,
         parent_id=parent_id,
-        is_markdown=True,
+        is_markdown=is_markdown,
+        enable_heading_anchors=enable_heading_anchors
+        if content_format == "markdown"
+        else False,
+        content_representation=content_representation,
     )
     result = page.to_simplified_dict()
     return json.dumps(
@@ -465,7 +500,10 @@ async def update_page(
     page_id: Annotated[str, Field(description="The ID of the page to update")],
     title: Annotated[str, Field(description="The new title of the page")],
     content: Annotated[
-        str, Field(description="The new content of the page in Markdown format")
+        str,
+        Field(
+            description="The new content of the page. Format depends on content_format parameter"
+        ),
     ],
     is_minor_edit: Annotated[
         bool, Field(description="Whether this is a minor edit", default=False)
@@ -477,6 +515,20 @@ async def update_page(
         str,
         Field(description="Optional the new parent page ID", default=""),
     ] = "",
+    content_format: Annotated[
+        str,
+        Field(
+            description="(Optional) The format of the content parameter. Options: 'markdown' (default), 'wiki', or 'storage'. Wiki format uses Confluence wiki markup syntax",
+            default="markdown",
+        ),
+    ] = "markdown",
+    enable_heading_anchors: Annotated[
+        bool,
+        Field(
+            description="(Optional) Whether to enable automatic heading anchor generation. Only applies when content_format is 'markdown'",
+            default=False,
+        ),
+    ] = False,
 ) -> str:
     """Update an existing Confluence page.
 
@@ -484,29 +536,50 @@ async def update_page(
         ctx: The FastMCP context.
         page_id: The ID of the page to update.
         title: The new title of the page.
-        content: The new content in Markdown format.
+        content: The new content of the page (format depends on content_format).
         is_minor_edit: Whether this is a minor edit.
         version_comment: Optional comment for this version.
         parent_id: Optional new parent page ID.
+        content_format: The format of the content ('markdown', 'wiki', or 'storage').
+        enable_heading_anchors: Whether to enable heading anchors (markdown only).
 
     Returns:
         JSON string representing the updated page object.
 
     Raises:
-        ValueError: If Confluence client is not configured or available.
+        ValueError: If Confluence client is not configured, available, or invalid content_format.
     """
     confluence_fetcher = await get_confluence_fetcher(ctx)
+
+    # Validate content_format
+    if content_format not in ["markdown", "wiki", "storage"]:
+        raise ValueError(
+            f"Invalid content_format: {content_format}. Must be 'markdown', 'wiki', or 'storage'"
+        )
+
     # TODO: revert this once Cursor IDE handles optional parameters with Union types correctly.
     actual_parent_id = parent_id if parent_id else None
 
+    # Determine parameters based on content format
+    if content_format == "markdown":
+        is_markdown = True
+        content_representation = None  # Will be converted to storage
+    else:
+        is_markdown = False
+        content_representation = content_format  # Pass 'wiki' or 'storage' directly
+
     updated_page = confluence_fetcher.update_page(
         page_id=page_id,
         title=title,
         body=content,
         is_minor_edit=is_minor_edit,
         version_comment=version_comment,
-        is_markdown=True,
+        is_markdown=is_markdown,
         parent_id=actual_parent_id,
+        enable_heading_anchors=enable_heading_anchors
+        if content_format == "markdown"
+        else False,
+        content_representation=content_representation,
     )
     page_data = updated_page.to_simplified_dict()
     return json.dumps(


Test patch:
diff --git a/tests/unit/confluence/test_pages.py b/tests/unit/confluence/test_pages.py
index fff5c603..4e494408 100644
--- a/tests/unit/confluence/test_pages.py
+++ b/tests/unit/confluence/test_pages.py
@@ -318,6 +318,49 @@ def test_create_page_error(self, pages_mixin):
         with pytest.raises(Exception, match="API Error"):
             pages_mixin.create_page("PROJ", "Test Page", "<p>Content</p>")
 
+    def test_create_page_with_wiki_format(self, pages_mixin):
+        """Test creating a new page with wiki markup format."""
+        # Arrange
+        space_key = "PROJ"
+        title = "Wiki Format Test Page"
+        wiki_body = "h1. This is a heading\n\n* Item 1\n* Item 2"
+
+        # Mock get_page_content to return a ConfluencePage
+        with patch.object(
+            pages_mixin,
+            "get_page_content",
+            return_value=ConfluencePage(
+                id="wiki123",
+                title=title,
+                content="Wiki page content",
+                space={"key": space_key, "name": "Project"},
+            ),
+        ):
+            # Act - use wiki format
+            result = pages_mixin.create_page(
+                space_key,
+                title,
+                wiki_body,
+                is_markdown=False,
+                content_representation="wiki",
+            )
+
+            # Assert
+            pages_mixin.confluence.create_page.assert_called_once_with(
+                space=space_key,
+                title=title,
+                body=wiki_body,  # Should be passed as-is
+                parent_id=None,
+                representation="wiki",  # Should use wiki representation
+            )
+
+            # Verify no markdown conversion happened
+            pages_mixin.preprocessor.markdown_to_confluence_storage.assert_not_called()
+
+            # Verify result is a ConfluencePage
+            assert isinstance(result, ConfluencePage)
+            assert result.id == "wiki123"
+
     def test_update_page_success(self, pages_mixin):
         """Test updating an existing page."""
         # Arrange
@@ -369,6 +412,52 @@ def test_update_page_error(self, pages_mixin):
         with pytest.raises(Exception, match="Failed to update page"):
             pages_mixin.update_page("987654321", "Test Page", "<p>Content</p>")
 
+    def test_update_page_with_wiki_format(self, pages_mixin):
+        """Test updating a page with wiki markup format."""
+        # Arrange
+        page_id = "wiki987"
+        title = "Updated Wiki Page"
+        wiki_body = "h1. Updated Heading\n\n||Header 1||Header 2||\n|Cell 1|Cell 2|"
+        version_comment = "Wiki format update"
+
+        # Mock get_page_content to return a document
+        mock_document = ConfluencePage(
+            id=page_id,
+            title=title,
+            content="Updated wiki content",
+            space={"key": "PROJ", "name": "Project"},
+            version={"number": 2},
+        )
+        with patch.object(pages_mixin, "get_page_content", return_value=mock_document):
+            # Act - use wiki format
+            result = pages_mixin.update_page(
+                page_id,
+                title,
+                wiki_body,
+                version_comment=version_comment,
+                is_markdown=False,
+                content_representation="wiki",
+            )
+
+            # Assert
+            pages_mixin.confluence.update_page.assert_called_once_with(
+                page_id=page_id,
+                title=title,
+                body=wiki_body,  # Should be passed as-is
+                type="page",
+                representation="wiki",  # Should use wiki representation
+                minor_edit=False,
+                version_comment=version_comment,
+                always_update=True,
+            )
+
+            # Verify no markdown conversion happened
+            pages_mixin.preprocessor.markdown_to_confluence_storage.assert_not_called()
+
+            # Verify result is a ConfluencePage
+            assert isinstance(result, ConfluencePage)
+            assert result.id == page_id
+
     def test_delete_page_success(self, pages_mixin):
         """Test successfully deleting a page."""
         # Arrange
@@ -832,6 +921,63 @@ def test_create_page_oauth_uses_v2_api(self, oauth_pages_mixin):
                 # Verify result is a ConfluencePage
                 assert isinstance(result, ConfluencePage)
                 assert result.id == "oauth_123456789"
+
+    def test_create_page_oauth_with_wiki_format(self, oauth_pages_mixin):
+        """Test that OAuth authentication uses v2 API for creating pages with wiki format."""
+        # Arrange
+        space_key = "PROJ"
+        title = "OAuth Wiki Test Page"
+        wiki_body = "h1. OAuth Wiki Test\n\n* Item 1\n* Item 2"
+
+        # Mock the v2 adapter
+        with patch(
+            "mcp_atlassian.confluence.pages.ConfluenceV2Adapter"
+        ) as mock_v2_adapter_class:
+            mock_v2_adapter = MagicMock()
+            mock_v2_adapter_class.return_value = mock_v2_adapter
+            mock_v2_adapter.create_page.return_value = {
+                "id": "oauth_wiki_123",
+                "title": title,
+            }
+
+            # Mock get_page_content to return a ConfluencePage
+            with patch.object(
+                oauth_pages_mixin,
+                "get_page_content",
+                return_value=ConfluencePage(
+                    id="oauth_wiki_123",
+                    title=title,
+                    content="OAuth wiki page content",
+                    space={"key": space_key, "name": "Project"},
+                ),
+            ):
+                # Act - use wiki format
+                result = oauth_pages_mixin.create_page(
+                    space_key,
+                    title,
+                    wiki_body,
+                    is_markdown=False,
+                    content_representation="wiki",
+                )
+
+                # Assert that v2 API was used with wiki representation
+                mock_v2_adapter.create_page.assert_called_once_with(
+                    space_key=space_key,
+                    title=title,
+                    body=wiki_body,
+                    parent_id=None,
+                    representation="wiki",
+                )
+
+                # Verify v1 API was NOT called
+                oauth_pages_mixin.confluence.create_page.assert_not_called()
+
+                # Verify no markdown conversion happened
+                oauth_pages_mixin.preprocessor.markdown_to_confluence_storage.assert_not_called()
+
+                # Verify result is a ConfluencePage
+                assert isinstance(result, ConfluencePage)
+                assert result.id == "oauth_wiki_123"
                 assert result.title == title
 
     def test_update_page_oauth_uses_v2_api(self, oauth_pages_mixin):