Skip to content

Commit d2813c6

Browse files
committed
Merge PR sooperset#705: add support to fetch previous versions of Confluence pages (resolving conflicts)
2 parents 8b0ffcd + fe329b0 commit d2813c6

File tree

12 files changed

+540
-12
lines changed

12 files changed

+540
-12
lines changed

.github/workflows/lint.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches: [ main ]
66
push:
77
branches: [ main ]
8+
workflow_dispatch:
89

910
jobs:
1011
lint:

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,6 @@ playground/
7575

7676
# Claude
7777
.claude/
78+
79+
# Credentials
80+
.test-credentials.json

PRD.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"results":[{"number":16,"message":"","minorEdit":false,"authorId":"712020:97e911b0-59b4-428d-b5ac-4ba8c19d410f","createdAt":"2025-09-18T17:26:52.657Z","page":{"body":{},"title":"PRD AWS MP Phase-3 - Supporting new business models","id":"1137248511"}},{"number":15,"message":"","minorEdit":false,"authorId":"70121:50e6052b-50f1-4410-871f-cc4da9b77743","createdAt":"2025-09-16T15:40:08.940Z","page":{"body":{},"title":"PRD AWS MP Phase-3 - Supporting new business models","id":"1137248511"}},{"number":14,"message":"","minorEdit":false,"authorId":"70121:50e6052b-50f1-4410-871f-cc4da9b77743","createdAt":"2025-09-15T12:27:03.473Z","page":{"body":{},"title":"PRD AWS MP Phase-3 - Supporting new business models","id":"1137248511"}},{"number":13,"message":"","minorEdit":false,"authorId":"70121:4c1cbb1c-ed3a-4a99-bf66-56affd80ee61","createdAt":"2025-09-15T10:08:00.245Z","page":{"body":{},"title":"PRD AWS MP Phase-3 - Supporting new business models","id":"1137248511"}},{"number":12,"message":"","minorEdit":false,"authorId":"70121:50e6052b-50f1-4410-871f-cc4da9b77743","createdAt":"2025-09-02T11:58:42.575Z","page":{"body":{},"title":"PRD AWS MP Phase-3 - Supporting new business models","id":"1137248511"}},{"number":11,"message":"","minorEdit":false,"authorId":"70121:50e6052b-50f1-4410-871f-cc4da9b77743","createdAt":"2025-07-23T13:13:49.875Z","page":{"body":{},"title":"PRD AWS MP Phase-3 - Supporting new business models","id":"1137248511"}},{"number":10,"message":"","minorEdit":false,"authorId":"70121:50e6052b-50f1-4410-871f-cc4da9b77743","createdAt":"2025-07-16T08:55:02.105Z","page":{"body":{},"title":"PRD AWS MP Phase-3 - Supporting new business models","id":"1137248511"}},{"number":9,"message":"","minorEdit":false,"authorId":"70121:50e6052b-50f1-4410-871f-cc4da9b77743","createdAt":"2025-07-07T05:18:16.570Z","page":{"body":{},"title":"PRD AWS MP Phase-3 - Supporting new business models","id":"1137248511"}},{"number":8,"message":"","minorEdit":false,"authorId":"70121:50e6052b-50f1-4410-871f-cc4da9b77743","createdAt":"2025-07-03T13:49:46.865Z","page":{"body":{},"title":"PRD AWS MP Phase-3 - Supporting new business models","id":"1137248511"}},{"number":7,"message":"","minorEdit":false,"authorId":"70121:50e6052b-50f1-4410-871f-cc4da9b77743","createdAt":"2025-06-23T03:58:56.972Z","page":{"body":{},"title":"PRD AWS MP Phase-3 - Supporting new business models","id":"1137248511"}},{"number":6,"message":"","minorEdit":false,"authorId":"70121:50e6052b-50f1-4410-871f-cc4da9b77743","createdAt":"2025-06-10T14:26:27.257Z","page":{"body":{},"title":"Draft - PRD AWS MP Phase-3 - Supporting new business models","id":"1137248511"}},{"number":5,"message":"","minorEdit":false,"authorId":"70121:50e6052b-50f1-4410-871f-cc4da9b77743","createdAt":"2025-06-10T14:25:02.828Z","page":{"body":{},"title":"Draft - PRD AWS MP Phase-3 - Supporting new business models","id":"1137248511"}},{"number":4,"message":"","minorEdit":false,"authorId":"70121:50e6052b-50f1-4410-871f-cc4da9b77743","createdAt":"2025-06-09T14:58:18.341Z","page":{"body":{},"title":"Draft - PRD AWS MP Phase-3 - Supporting new business models","id":"1137248511"}},{"number":3,"message":"","minorEdit":false,"authorId":"70121:50e6052b-50f1-4410-871f-cc4da9b77743","createdAt":"2025-06-09T14:25:56.886Z","page":{"body":{},"title":"Draft - PRD AWS MP Phase-3 - Supporting new business models","id":"1137248511"}},{"number":2,"message":"","minorEdit":false,"authorId":"70121:50e6052b-50f1-4410-871f-cc4da9b77743","createdAt":"2025-06-09T13:02:34.619Z","page":{"body":{},"title":"Draft - PRD AWS MP Phase-3 - Supporting new business models","id":"1137248511"}},{"number":1,"message":"","minorEdit":false,"authorId":"70121:50e6052b-50f1-4410-871f-cc4da9b77743","createdAt":"2025-06-09T12:19:51.152Z","page":{"body":{},"title":"Draft - PRD AWS MP Phase-3 - Supporting new business models","id":"1137248511"}}],"_links":{"base":"https://here-technologies.atlassian.net/wiki"}}

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -786,7 +786,8 @@ Here's a complete example of setting up multi-user authentication with streamabl
786786
#### Confluence Tools
787787

788788
- `confluence_search`: Search Confluence content using CQL
789-
- `confluence_get_page`: Get content of a specific page
789+
- `confluence_get_page`: Get content of a specific page (supports version history)
790+
- `get_page_versions`: Get version information for a page
790791
- `confluence_create_page`: Create a new page
791792
- `confluence_update_page`: Update an existing page
792793
<<<<<<< HEAD
@@ -801,8 +802,14 @@ Here's a complete example of setting up multi-user authentication with streamabl
801802
| --------------- | ------------------------------- | -------------------------------- |
802803
| **Read** | `jira_search` | `confluence_search` |
803804
<<<<<<< HEAD
805+
<<<<<<< HEAD
804806
| | `jira_get_issue` | `confluence_get_page` |
805807
| | `jira_get_all_projects` | `confluence_get_page_children` |
808+
=======
809+
| | `jira_get_issue` | `confluence_get_page` (w/ versions) |
810+
| | `jira_get_all_projects` | `get_page_versions` |
811+
| | `jira_get_project_issues` | `confluence_get_page_children` |
812+
>>>>>>> maxheadroom/main
806813
| | `jira_get_project_issues` | `confluence_get_comments` |
807814
| | `jira_get_worklog` | `confluence_get_labels` |
808815
| | `jira_get_transitions` | `confluence_search_user` |

src/mcp_atlassian/confluence/pages.py

Lines changed: 153 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from requests.exceptions import HTTPError
77

88
from ..exceptions import MCPAtlassianAuthenticationError
9-
from ..models.confluence import ConfluencePage
9+
from ..models.confluence import ConfluencePage, ConfluenceVersion
1010
from .client import ConfluenceClient
1111
from .v2_adapter import ConfluenceV2Adapter
1212

@@ -30,7 +30,11 @@ def _v2_adapter(self) -> ConfluenceV2Adapter | None:
3030
return None
3131

3232
def get_page_content(
33-
self, page_id: str, *, convert_to_markdown: bool = True
33+
self,
34+
page_id: str,
35+
*,
36+
convert_to_markdown: bool = True,
37+
version: int | None = None,
3438
) -> ConfluencePage:
3539
"""
3640
Get content of a specific page.
@@ -39,12 +43,15 @@ def get_page_content(
3943
page_id: The ID of the page to retrieve
4044
convert_to_markdown: When True, returns content in markdown format,
4145
otherwise returns raw HTML (keyword-only)
46+
version: Optional version number of the page to retrieve. If None, gets the
47+
latest version (keyword-only)
4248
4349
Returns:
4450
ConfluencePage model containing the page content and metadata
4551
4652
Raises:
47-
MCPAtlassianAuthenticationError: If authentication fails with the Confluence API (401/403)
53+
MCPAtlassianAuthenticationError: If authentication fails with the
54+
Confluence API (401/403)
4855
Exception: If there is an error retrieving the page
4956
"""
5057
try:
@@ -53,19 +60,28 @@ def get_page_content(
5360
if v2_adapter:
5461
logger.debug(
5562
f"Using v2 API for OAuth authentication to get page '{page_id}'"
63+
+ (f" version {version}" if version else "")
5664
)
5765
page = v2_adapter.get_page(
5866
page_id=page_id,
5967
expand="body.storage,version,space,children.attachment",
68+
version=version,
6069
)
6170
else:
71+
version_msg = f" version {version}" if version else ""
6272
logger.debug(
63-
f"Using v1 API for token/basic authentication to get page '{page_id}'"
64-
)
65-
page = self.confluence.get_page_by_id(
66-
page_id=page_id,
67-
expand="body.storage,version,space,children.attachment",
73+
f"Using v1 API for token/basic authentication to get page "
74+
f"'{page_id}'{version_msg}"
6875
)
76+
# For v1 API, we need to add version parameter if specified
77+
get_page_kwargs = {
78+
"page_id": page_id,
79+
"expand": "body.storage,version,space,children.attachment",
80+
}
81+
if version is not None:
82+
get_page_kwargs["version"] = version
83+
84+
page = self.confluence.get_page_by_id(**get_page_kwargs)
6985

7086
space_key = page.get("space", {}).get("key", "")
7187
content = page["body"]["storage"]["value"]
@@ -106,6 +122,135 @@ def get_page_content(
106122
)
107123
raise Exception(f"Error retrieving page content: {str(e)}") from e
108124

125+
def get_page_versions(self, page_id: str) -> list[ConfluenceVersion]:
126+
"""
127+
Get all versions of a specific page.
128+
129+
Args:
130+
page_id: The ID of the page
131+
132+
Returns:
133+
List of ConfluenceVersion objects
134+
"""
135+
try:
136+
if v2_adapter := self._v2_adapter:
137+
logger.debug(f"Using v2 API to get versions for page '{page_id}'")
138+
return v2_adapter.get_page_versions(page_id)
139+
else:
140+
logger.debug(f"Using v1 API to get versions for page '{page_id}'")
141+
# For Cloud instances, try the wiki prefix
142+
base_url = self.config.url
143+
if "/wiki" not in base_url:
144+
base_url = f"{base_url}/wiki"
145+
146+
try:
147+
# Use the proper versions endpoint with pagination
148+
versions = []
149+
start = 0
150+
limit = 50
151+
152+
while True:
153+
url = f"{base_url}/rest/api/content/{page_id}/version"
154+
params = {"start": start, "limit": limit}
155+
response = self.confluence._session.get(url, params=params)
156+
response.raise_for_status()
157+
158+
data = response.json()
159+
results = data.get("results", [])
160+
161+
if not results:
162+
break
163+
164+
for version_data in results:
165+
versions.append(
166+
ConfluenceVersion.from_api_response(version_data)
167+
)
168+
169+
# Check if there are more results
170+
if len(results) < limit:
171+
break
172+
173+
start += limit
174+
175+
return versions
176+
except Exception as e:
177+
logger.warning(f"Failed to get versions via v1 API: {e}")
178+
# Fallback: get current version only
179+
page = self.confluence.get_page_by_id(
180+
page_id=page_id, expand="version"
181+
)
182+
183+
versions = []
184+
if version_data := page.get("version"):
185+
versions.append(
186+
ConfluenceVersion.from_api_response(version_data)
187+
)
188+
189+
return versions
190+
191+
except HTTPError as e:
192+
if e.response.status_code == 401:
193+
raise MCPAtlassianAuthenticationError(
194+
"Authentication failed when getting page versions"
195+
) from e
196+
logger.error(f"HTTP error getting versions for page '{page_id}': {e}")
197+
raise ValueError(f"Failed to get versions for page '{page_id}': {e}") from e
198+
except Exception as e:
199+
logger.error(f"Error getting versions for page '{page_id}': {e}")
200+
raise ValueError(f"Failed to get versions for page '{page_id}': {e}") from e
201+
202+
def get_page_version(self, page_id: str, version_number: int) -> ConfluenceVersion:
203+
"""
204+
Get a specific version of a page.
205+
206+
Args:
207+
page_id: The ID of the page
208+
version_number: The version number to retrieve
209+
210+
Returns:
211+
ConfluenceVersion object
212+
"""
213+
try:
214+
if v2_adapter := self._v2_adapter:
215+
logger.debug(
216+
f"Using v2 API to get version {version_number} for page '{page_id}'"
217+
)
218+
return v2_adapter.get_page_version(page_id, version_number)
219+
else:
220+
logger.debug(
221+
f"Using v1 API to get version {version_number} for page '{page_id}'"
222+
)
223+
# Get page with specific version
224+
page = self.confluence.get_page_by_id(
225+
page_id=page_id, version=version_number, expand="version"
226+
)
227+
228+
if version_data := page.get("version"):
229+
return ConfluenceVersion.from_api_response(version_data)
230+
231+
raise ValueError(
232+
f"Version {version_number} not found for page '{page_id}'"
233+
)
234+
235+
except HTTPError as e:
236+
if e.response.status_code == 401:
237+
raise MCPAtlassianAuthenticationError(
238+
"Authentication failed when getting page version"
239+
) from e
240+
logger.error(
241+
f"HTTP error getting version {version_number} for page '{page_id}': {e}"
242+
)
243+
raise ValueError(
244+
f"Failed to get version {version_number} for page '{page_id}': {e}"
245+
) from e
246+
except Exception as e:
247+
logger.error(
248+
f"Error getting version {version_number} for page '{page_id}': {e}"
249+
)
250+
raise ValueError(
251+
f"Failed to get version {version_number} for page '{page_id}': {e}"
252+
) from e
253+
109254
def get_page_ancestors(self, page_id: str) -> list[ConfluencePage]:
110255
"""
111256
Get ancestors (parent pages) of a specific page.

src/mcp_atlassian/confluence/v2_adapter.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66
"""
77

88
import logging
9-
from typing import Any
9+
from typing import TYPE_CHECKING, Any
1010

1111
import requests
1212
from requests.exceptions import HTTPError
1313

14+
if TYPE_CHECKING:
15+
from ..models.confluence import ConfluenceVersion
16+
1417
logger = logging.getLogger("mcp-atlassian")
1518

1619

@@ -164,6 +167,81 @@ def _get_page_version(self, page_id: str) -> int:
164167
logger.error(f"Error getting page version for '{page_id}': {e}")
165168
raise ValueError(f"Failed to get page version for '{page_id}': {e}") from e
166169

170+
def get_page_versions(self, page_id: str) -> list:
171+
"""Get all versions of a page using v2 API.
172+
173+
Args:
174+
page_id: The ID of the page
175+
176+
Returns:
177+
List of ConfluenceVersion objects
178+
179+
Raises:
180+
ValueError: If API call fails
181+
"""
182+
try:
183+
url = f"{self.base_url}/api/v2/pages/{page_id}/versions"
184+
response = self.session.get(url)
185+
response.raise_for_status()
186+
187+
data = response.json()
188+
versions = []
189+
190+
from ..models.confluence import ConfluenceVersion
191+
192+
for version_data in data.get("results", []):
193+
versions.append(ConfluenceVersion.from_api_response(version_data))
194+
195+
return versions
196+
197+
except HTTPError as e:
198+
logger.error(f"HTTP error getting versions for page '{page_id}': {e}")
199+
raise ValueError(f"Failed to get versions for page '{page_id}': {e}") from e
200+
except Exception as e:
201+
logger.error(f"Error getting versions for page '{page_id}': {e}")
202+
raise ValueError(f"Failed to get versions for page '{page_id}': {e}") from e
203+
204+
def get_page_version(
205+
self, page_id: str, version_number: int
206+
) -> "ConfluenceVersion":
207+
"""Get a specific version of a page using v2 API.
208+
209+
Args:
210+
page_id: The ID of the page
211+
version_number: The version number to retrieve
212+
213+
Returns:
214+
ConfluenceVersion object
215+
216+
Raises:
217+
ValueError: If API call fails
218+
"""
219+
try:
220+
url = f"{self.base_url}/api/v2/pages/{page_id}/versions/{version_number}"
221+
response = self.session.get(url)
222+
response.raise_for_status()
223+
224+
data = response.json()
225+
226+
from ..models.confluence import ConfluenceVersion
227+
228+
return ConfluenceVersion.from_api_response(data)
229+
230+
except HTTPError as e:
231+
logger.error(
232+
f"HTTP error getting version {version_number} for page '{page_id}': {e}"
233+
)
234+
raise ValueError(
235+
f"Failed to get version {version_number} for page '{page_id}': {e}"
236+
) from e
237+
except Exception as e:
238+
logger.error(
239+
f"Error getting version {version_number} for page '{page_id}': {e}"
240+
)
241+
raise ValueError(
242+
f"Failed to get version {version_number} for page '{page_id}': {e}"
243+
) from e
244+
167245
def update_page(
168246
self,
169247
page_id: str,
@@ -276,12 +354,14 @@ def get_page(
276354
self,
277355
page_id: str,
278356
expand: str | None = None,
357+
version: int | None = None,
279358
) -> dict[str, Any]:
280359
"""Get a page using the v2 API.
281360
282361
Args:
283362
page_id: The ID of the page to retrieve
284363
expand: Fields to expand in the response (not used in v2 API, for compatibility only)
364+
version: Optional version number of the page to retrieve
285365
286366
Returns:
287367
The page data from the API response in v1-compatible format
@@ -295,6 +375,8 @@ def get_page(
295375

296376
# Convert v1 expand parameters to v2 format
297377
params = {"body-format": "storage"}
378+
if version is not None:
379+
params["version"] = version
298380

299381
response = self.session.get(url, params=params)
300382
response.raise_for_status()

src/mcp_atlassian/models/confluence/page.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class ConfluenceVersion(ApiModel, TimestampMixin):
3131
when: str = EMPTY_STRING
3232
message: str | None = None
3333
by: ConfluenceUser | None = None
34+
minor_edit: bool = False
3435

3536
@classmethod
3637
def from_api_response(
@@ -57,6 +58,7 @@ def from_api_response(
5758
when=data.get("when", EMPTY_STRING),
5859
message=data.get("message"),
5960
by=by_user,
61+
minor_edit=data.get("minorEdit", False),
6062
)
6163

6264
def to_simplified_dict(self) -> dict[str, Any]:

0 commit comments

Comments
 (0)