apron-tools 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. apron_tools/__init__.py +43 -0
  2. apron_tools/_utils.py +26 -0
  3. apron_tools/fileio.py +75 -0
  4. apron_tools/providers/__init__.py +1 -0
  5. apron_tools/providers/atlassian/__init__.py +6 -0
  6. apron_tools/providers/atlassian/confluence/__init__.py +4 -0
  7. apron_tools/providers/atlassian/confluence/scopes.py +51 -0
  8. apron_tools/providers/atlassian/confluence/tools.py +433 -0
  9. apron_tools/providers/atlassian/confluence/types.py +355 -0
  10. apron_tools/providers/atlassian/jira/__init__.py +4 -0
  11. apron_tools/providers/atlassian/jira/scopes.py +51 -0
  12. apron_tools/providers/atlassian/jira/tools.py +644 -0
  13. apron_tools/providers/atlassian/jira/types.py +530 -0
  14. apron_tools/providers/github/__init__.py +4 -0
  15. apron_tools/providers/github/scopes.py +53 -0
  16. apron_tools/providers/github/tools.py +1066 -0
  17. apron_tools/providers/github/types.py +1059 -0
  18. apron_tools/providers/google/__init__.py +10 -0
  19. apron_tools/providers/google/_images.py +147 -0
  20. apron_tools/providers/google/calendar/__init__.py +5 -0
  21. apron_tools/providers/google/calendar/scopes.py +39 -0
  22. apron_tools/providers/google/calendar/tools.py +363 -0
  23. apron_tools/providers/google/calendar/types.py +284 -0
  24. apron_tools/providers/google/docs/__init__.py +6 -0
  25. apron_tools/providers/google/docs/scopes.py +45 -0
  26. apron_tools/providers/google/docs/tools.py +710 -0
  27. apron_tools/providers/google/docs/types.py +571 -0
  28. apron_tools/providers/google/drive/__init__.py +5 -0
  29. apron_tools/providers/google/drive/scopes.py +35 -0
  30. apron_tools/providers/google/drive/tools.py +510 -0
  31. apron_tools/providers/google/drive/types.py +357 -0
  32. apron_tools/providers/google/gmail/__init__.py +5 -0
  33. apron_tools/providers/google/gmail/scopes.py +71 -0
  34. apron_tools/providers/google/gmail/tools.py +695 -0
  35. apron_tools/providers/google/gmail/types.py +426 -0
  36. apron_tools/providers/google/sheets/__init__.py +6 -0
  37. apron_tools/providers/google/sheets/scopes.py +42 -0
  38. apron_tools/providers/google/sheets/tools.py +407 -0
  39. apron_tools/providers/google/sheets/types.py +433 -0
  40. apron_tools/providers/google/slides/__init__.py +6 -0
  41. apron_tools/providers/google/slides/scopes.py +48 -0
  42. apron_tools/providers/google/slides/tools.py +1145 -0
  43. apron_tools/providers/google/slides/types.py +527 -0
  44. apron_tools/providers/google/tasks/__init__.py +5 -0
  45. apron_tools/providers/google/tasks/scopes.py +46 -0
  46. apron_tools/providers/google/tasks/tools.py +329 -0
  47. apron_tools/providers/google/tasks/types.py +275 -0
  48. apron_tools/providers/hubspot/__init__.py +4 -0
  49. apron_tools/providers/hubspot/scopes.py +93 -0
  50. apron_tools/providers/hubspot/tools.py +688 -0
  51. apron_tools/providers/hubspot/types.py +411 -0
  52. apron_tools/providers/linear/__init__.py +4 -0
  53. apron_tools/providers/linear/scopes.py +53 -0
  54. apron_tools/providers/linear/tools.py +886 -0
  55. apron_tools/providers/linear/types.py +613 -0
  56. apron_tools/providers/microsoft/__init__.py +10 -0
  57. apron_tools/providers/microsoft/excel/__init__.py +4 -0
  58. apron_tools/providers/microsoft/excel/scopes.py +41 -0
  59. apron_tools/providers/microsoft/excel/tools.py +411 -0
  60. apron_tools/providers/microsoft/excel/types.py +298 -0
  61. apron_tools/providers/microsoft/onedrive/__init__.py +24 -0
  62. apron_tools/providers/microsoft/onedrive/_shared.py +253 -0
  63. apron_tools/providers/microsoft/onedrive/scopes.py +39 -0
  64. apron_tools/providers/microsoft/onedrive/tools.py +334 -0
  65. apron_tools/providers/microsoft/onedrive/types.py +286 -0
  66. apron_tools/providers/microsoft/outlook/__init__.py +4 -0
  67. apron_tools/providers/microsoft/outlook/scopes.py +46 -0
  68. apron_tools/providers/microsoft/outlook/tools.py +245 -0
  69. apron_tools/providers/microsoft/outlook/types.py +246 -0
  70. apron_tools/providers/microsoft/powerpoint/__init__.py +6 -0
  71. apron_tools/providers/microsoft/powerpoint/presentation.py +218 -0
  72. apron_tools/providers/microsoft/powerpoint/scopes.py +52 -0
  73. apron_tools/providers/microsoft/powerpoint/tools.py +268 -0
  74. apron_tools/providers/microsoft/powerpoint/types.py +259 -0
  75. apron_tools/providers/microsoft/sharepoint/__init__.py +4 -0
  76. apron_tools/providers/microsoft/sharepoint/scopes.py +47 -0
  77. apron_tools/providers/microsoft/sharepoint/tools.py +306 -0
  78. apron_tools/providers/microsoft/sharepoint/types.py +362 -0
  79. apron_tools/providers/microsoft/teams/__init__.py +5 -0
  80. apron_tools/providers/microsoft/teams/scopes.py +73 -0
  81. apron_tools/providers/microsoft/teams/tools.py +360 -0
  82. apron_tools/providers/microsoft/teams/types.py +415 -0
  83. apron_tools/providers/microsoft/word/__init__.py +6 -0
  84. apron_tools/providers/microsoft/word/document.py +93 -0
  85. apron_tools/providers/microsoft/word/scopes.py +49 -0
  86. apron_tools/providers/microsoft/word/tools.py +218 -0
  87. apron_tools/providers/microsoft/word/types.py +209 -0
  88. apron_tools/providers/notion/__init__.py +4 -0
  89. apron_tools/providers/notion/scopes.py +60 -0
  90. apron_tools/providers/notion/tools.py +738 -0
  91. apron_tools/providers/notion/types.py +564 -0
  92. apron_tools/providers/salesforce/__init__.py +4 -0
  93. apron_tools/providers/salesforce/scopes.py +50 -0
  94. apron_tools/providers/salesforce/tools.py +377 -0
  95. apron_tools/providers/salesforce/types.py +280 -0
  96. apron_tools/providers/slack/__init__.py +33 -0
  97. apron_tools/providers/slack/scopes.py +226 -0
  98. apron_tools/providers/slack/tools.py +1224 -0
  99. apron_tools/providers/slack/types.py +770 -0
  100. apron_tools/providers/trello/__init__.py +4 -0
  101. apron_tools/providers/trello/scopes.py +41 -0
  102. apron_tools/providers/trello/tools.py +358 -0
  103. apron_tools/providers/trello/types.py +337 -0
  104. apron_tools/providers/typeform/__init__.py +4 -0
  105. apron_tools/providers/typeform/scopes.py +46 -0
  106. apron_tools/providers/typeform/tools.py +255 -0
  107. apron_tools/providers/typeform/types.py +345 -0
  108. apron_tools/providers/web_access/__init__.py +6 -0
  109. apron_tools/providers/web_access/scopes.py +41 -0
  110. apron_tools/providers/web_access/ssrf.py +76 -0
  111. apron_tools/providers/web_access/tools.py +180 -0
  112. apron_tools/providers/web_access/types.py +66 -0
  113. apron_tools/py.typed +0 -0
  114. apron_tools/registry.py +135 -0
  115. apron_tools/tool.py +65 -0
  116. apron_tools/types.py +270 -0
  117. apron_tools-0.1.0.dist-info/METADATA +211 -0
  118. apron_tools-0.1.0.dist-info/RECORD +121 -0
  119. apron_tools-0.1.0.dist-info/WHEEL +5 -0
  120. apron_tools-0.1.0.dist-info/licenses/LICENSE +201 -0
  121. apron_tools-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,43 @@
1
+ """apron-tools: Agent-ready provider API wrappers.
2
+
3
+ Curated tool definitions with typed schemas, OAuth scope mappings,
4
+ and provider SDK wrappers for LLM function calling.
5
+ """
6
+
7
+ from apron_tools.fileio import resolve_file_input
8
+ from apron_tools.registry import (
9
+ discover_capability_groups,
10
+ discover_tools,
11
+ get_tools_for_provider,
12
+ get_tools_for_service,
13
+ )
14
+ from apron_tools.tool import tool
15
+ from apron_tools.types import (
16
+ AccessType,
17
+ CapabilityGroup,
18
+ FileFromBytes,
19
+ FileFromUrl,
20
+ FileInput,
21
+ Scope,
22
+ ScopeMetadata,
23
+ ToolDefinition,
24
+ ToolResult,
25
+ )
26
+
27
+ __all__ = [
28
+ "AccessType",
29
+ "CapabilityGroup",
30
+ "FileFromBytes",
31
+ "FileFromUrl",
32
+ "FileInput",
33
+ "Scope",
34
+ "ScopeMetadata",
35
+ "ToolDefinition",
36
+ "ToolResult",
37
+ "discover_capability_groups",
38
+ "discover_tools",
39
+ "get_tools_for_provider",
40
+ "get_tools_for_service",
41
+ "resolve_file_input",
42
+ "tool",
43
+ ]
apron_tools/_utils.py ADDED
@@ -0,0 +1,26 @@
1
+ """Shared string utilities for provider tool implementations."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def parse_csv_ids(value: str) -> list[str]:
7
+ """Split a comma-separated string into a list of non-empty, stripped tokens.
8
+
9
+ Args:
10
+ value: A comma-separated string (e.g. ``"abc,def , ghi"``).
11
+
12
+ Returns:
13
+ A list of stripped, non-empty strings.
14
+ Returns an empty list when *value* is empty or contains only whitespace.
15
+
16
+ Examples:
17
+ >>> parse_csv_ids("abc123,def456")
18
+ ['abc123', 'def456']
19
+ >>> parse_csv_ids("abc123, def456 , ghi789")
20
+ ['abc123', 'def456', 'ghi789']
21
+ >>> parse_csv_ids("")
22
+ []
23
+ >>> parse_csv_ids(" , ")
24
+ []
25
+ """
26
+ return [item.strip() for item in value.split(",") if item.strip()]
apron_tools/fileio.py ADDED
@@ -0,0 +1,75 @@
1
+ """Shared file I/O for resolving FileInput to raw bytes.
2
+
3
+ Tools that accept file uploads use this module to normalise
4
+ ``FileFromUrl`` and ``FileFromBytes`` into a common
5
+ ``(bytes, filename, mime_type)`` tuple.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from email.message import Message
11
+ from urllib.parse import unquote, urlparse
12
+
13
+ import httpx
14
+
15
+ from apron_tools.types import FileFromBytes, FileFromUrl, FileInput
16
+
17
+ _TIMEOUT = 60.0
18
+
19
+
20
+ async def resolve_file_input(file: FileInput) -> tuple[bytes, str, str]:
21
+ """Resolve a FileInput to raw bytes, filename, and MIME type.
22
+
23
+ For ``FileFromBytes``: returns the data directly.
24
+ For ``FileFromUrl``: downloads the URL and infers filename/MIME type
25
+ from the response headers when not explicitly provided.
26
+
27
+ .. warning:: Security consideration
28
+
29
+ When resolving a ``FileFromUrl``, the URL is fetched as-is with
30
+ no network filtering or size limits. See the warning on
31
+ ``FileFromUrl`` for details. Callers exposing this to untrusted
32
+ input should validate URLs before passing them here.
33
+
34
+ Args:
35
+ file: A ``FileFromUrl`` or ``FileFromBytes`` instance.
36
+
37
+ Returns:
38
+ A tuple of (bytes, filename, mime_type).
39
+
40
+ Raises:
41
+ httpx.HTTPStatusError: If the URL download fails.
42
+ """
43
+ if isinstance(file, FileFromBytes):
44
+ return file.data, file.filename, file.mime_type
45
+
46
+ return await _download_url(file)
47
+
48
+
49
+ async def _download_url(file: FileFromUrl) -> tuple[bytes, str, str]:
50
+ """Download a file from a URL and infer metadata from the response."""
51
+ async with httpx.AsyncClient(timeout=_TIMEOUT, follow_redirects=True) as client:
52
+ resp = await client.get(str(file.url))
53
+ resp.raise_for_status()
54
+
55
+ data = resp.content
56
+
57
+ # Infer filename from explicit override, Content-Disposition, or URL path.
58
+ filename = file.filename
59
+ if not filename:
60
+ disposition = resp.headers.get("content-disposition", "")
61
+ if disposition:
62
+ msg = Message()
63
+ msg["content-disposition"] = disposition
64
+ # Prefer RFC 5987 filename* over plain filename.
65
+ raw = msg.get_param("filename*") or msg.get_filename() or ""
66
+ filename = str(raw) if raw else ""
67
+ if not filename:
68
+ filename = unquote(urlparse(str(file.url)).path.rsplit("/", 1)[-1]) or "download"
69
+
70
+ # Infer MIME type from explicit override or Content-Type header.
71
+ mime_type = file.mime_type
72
+ if not mime_type:
73
+ mime_type = resp.headers.get("content-type", "application/octet-stream").split(";")[0].strip()
74
+
75
+ return data, filename, mime_type
@@ -0,0 +1 @@
1
+ """Provider subpackages for apron-tools."""
@@ -0,0 +1,6 @@
1
+ """Atlassian provider.
2
+
3
+ API docs:
4
+ - Jira: https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/
5
+ - Confluence: https://developer.atlassian.com/cloud/confluence/rest/v2/intro/
6
+ """
@@ -0,0 +1,4 @@
1
+ """Atlassian Confluence provider.
2
+
3
+ API docs: https://developer.atlassian.com/cloud/confluence/rest/v2/intro/
4
+ """
@@ -0,0 +1,51 @@
1
+ """OAuth scope definitions for Atlassian Confluence tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from apron_tools.types import CapabilityGroup, Scope
6
+
7
+
8
+ class ConfluenceScope(Scope):
9
+ """OAuth scopes for Atlassian Confluence API access."""
10
+
11
+ READ_CONFLUENCE_CONTENT = (
12
+ "read:confluence-content.all",
13
+ "Read Content",
14
+ "View Confluence pages, blog posts, comments, and attachments across all spaces",
15
+ "read",
16
+ False,
17
+ )
18
+ WRITE_CONFLUENCE_CONTENT = (
19
+ "write:confluence-content",
20
+ "Write Content",
21
+ "Create and modify Confluence pages, blog posts, and attachments",
22
+ "write",
23
+ False,
24
+ )
25
+ SEARCH_CONFLUENCE = (
26
+ "search:confluence",
27
+ "Search",
28
+ "Search across Confluence content",
29
+ "read",
30
+ False,
31
+ )
32
+
33
+
34
+ SCOPES: dict[str, list[ConfluenceScope]] = {
35
+ "atlassian_confluence_explore_spaces": [ConfluenceScope.READ_CONFLUENCE_CONTENT],
36
+ "atlassian_confluence_get_page_content": [ConfluenceScope.READ_CONFLUENCE_CONTENT],
37
+ "atlassian_confluence_create_page": [ConfluenceScope.WRITE_CONFLUENCE_CONTENT],
38
+ "atlassian_confluence_update_page": [
39
+ ConfluenceScope.READ_CONFLUENCE_CONTENT,
40
+ ConfluenceScope.WRITE_CONFLUENCE_CONTENT,
41
+ ],
42
+ "atlassian_confluence_search_content": [ConfluenceScope.SEARCH_CONFLUENCE],
43
+ "atlassian_confluence_get_child_pages": [ConfluenceScope.READ_CONFLUENCE_CONTENT],
44
+ "atlassian_confluence_upload_attachment": [ConfluenceScope.WRITE_CONFLUENCE_CONTENT],
45
+ }
46
+
47
+ CAPABILITY_GROUP = CapabilityGroup(
48
+ provider="atlassian_confluence",
49
+ display_name="Atlassian Confluence",
50
+ scopes=sorted({s for scopes in SCOPES.values() for s in scopes}),
51
+ )
@@ -0,0 +1,433 @@
1
+ """Atlassian Confluence tool functions for interacting with the Confluence REST API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+
7
+ from apron_tools.fileio import resolve_file_input
8
+ from apron_tools.providers.atlassian.confluence.types import (
9
+ ChildPageSummary,
10
+ CreatePageParams,
11
+ CreatePageResult,
12
+ ExploreSpacesParams,
13
+ ExploreSpacesResult,
14
+ GetChildPagesParams,
15
+ GetChildPagesResult,
16
+ GetPageContentParams,
17
+ GetPageContentResult,
18
+ PageSummary,
19
+ SearchContentParams,
20
+ SearchContentResult,
21
+ SearchResult,
22
+ SpaceSummary,
23
+ UpdatePageParams,
24
+ UpdatePageResult,
25
+ UploadAttachmentParams,
26
+ UploadAttachmentResult,
27
+ )
28
+ from apron_tools.tool import tool
29
+
30
+ from .scopes import SCOPES
31
+
32
+ _BASE_URL = "https://api.atlassian.com"
33
+ _TIMEOUT = 60.0
34
+
35
+
36
+ def _headers(token: str, *, content_type: bool = False) -> dict[str, str]:
37
+ """Build authorization headers for a Confluence API request."""
38
+ h: dict[str, str] = {
39
+ "Authorization": f"Bearer {token}",
40
+ "Accept": "application/json",
41
+ }
42
+ if content_type:
43
+ h["Content-Type"] = "application/json"
44
+ return h
45
+
46
+
47
+ async def _resolve_cloud_id(token: str, base_url: str) -> str | None:
48
+ """Resolve the Confluence cloud ID for the authenticated user.
49
+
50
+ Atlassian cloud APIs require a cloud ID to construct API URLs. This
51
+ calls the accessible-resources endpoint to retrieve it.
52
+ """
53
+ try:
54
+ async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
55
+ resp = await client.get(
56
+ f"{base_url}/oauth/token/accessible-resources",
57
+ headers=_headers(token),
58
+ )
59
+ if resp.is_success:
60
+ resources = resp.json()
61
+ if resources:
62
+ return resources[0].get("id")
63
+ except httpx.HTTPError:
64
+ pass
65
+ return None
66
+
67
+
68
+ def _api_v2_url(cloud_id: str, path: str, *, base_url: str) -> str:
69
+ """Build a Confluence REST API v2 URL."""
70
+ return f"{base_url}/ex/confluence/{cloud_id}/wiki/api/v2{path}"
71
+
72
+
73
+ def _api_v1_url(cloud_id: str, path: str, *, base_url: str) -> str:
74
+ """Build a Confluence REST API v1 URL."""
75
+ return f"{base_url}/ex/confluence/{cloud_id}/wiki/rest/api{path}"
76
+
77
+
78
+ @tool(
79
+ scopes=SCOPES["atlassian_confluence_explore_spaces"],
80
+ api_docs="https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-space/#api-wiki-api-v2-spaces-get",
81
+ provider="atlassian",
82
+ service="atlassian_confluence",
83
+ )
84
+ async def atlassian_confluence_explore_spaces(
85
+ params: ExploreSpacesParams,
86
+ *,
87
+ token: str,
88
+ base_url: str = _BASE_URL,
89
+ ) -> ExploreSpacesResult:
90
+ """List all Confluence spaces accessible to the authenticated user."""
91
+ cloud_id = await _resolve_cloud_id(token, base_url)
92
+ if not cloud_id:
93
+ return ExploreSpacesResult(
94
+ success=False,
95
+ error="Failed to resolve Confluence cloud ID. Ensure you have access to a Confluence site.",
96
+ )
97
+
98
+ try:
99
+ async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
100
+ resp = await client.get(
101
+ _api_v2_url(cloud_id, "/spaces", base_url=base_url),
102
+ headers=_headers(token),
103
+ params={"limit": params.max_results},
104
+ )
105
+ except httpx.HTTPError as exc:
106
+ return ExploreSpacesResult(success=False, error=str(exc))
107
+
108
+ if not resp.is_success:
109
+ return ExploreSpacesResult(
110
+ success=False,
111
+ error=f"Confluence API error {resp.status_code}: {resp.text}",
112
+ )
113
+
114
+ data = resp.json()
115
+ spaces = [SpaceSummary.model_validate(s) for s in data.get("results", [])]
116
+ return ExploreSpacesResult(success=True, spaces=spaces)
117
+
118
+
119
+ @tool(
120
+ scopes=SCOPES["atlassian_confluence_get_page_content"],
121
+ api_docs="https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-wiki-api-v2-pages-id-get",
122
+ provider="atlassian",
123
+ service="atlassian_confluence",
124
+ )
125
+ async def atlassian_confluence_get_page_content(
126
+ params: GetPageContentParams,
127
+ *,
128
+ token: str,
129
+ base_url: str = _BASE_URL,
130
+ ) -> GetPageContentResult:
131
+ """Retrieve a Confluence page by ID, including its storage-format body."""
132
+ cloud_id = await _resolve_cloud_id(token, base_url)
133
+ if not cloud_id:
134
+ return GetPageContentResult(
135
+ success=False,
136
+ error="Failed to resolve Confluence cloud ID. Ensure you have access to a Confluence site.",
137
+ )
138
+
139
+ try:
140
+ async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
141
+ resp = await client.get(
142
+ _api_v2_url(cloud_id, f"/pages/{params.page_id}", base_url=base_url),
143
+ headers=_headers(token),
144
+ params={"body-format": "storage"},
145
+ )
146
+ except httpx.HTTPError as exc:
147
+ return GetPageContentResult(success=False, error=str(exc))
148
+
149
+ if not resp.is_success:
150
+ return GetPageContentResult(
151
+ success=False,
152
+ error=f"Confluence API error {resp.status_code}: {resp.text}",
153
+ )
154
+
155
+ page = PageSummary.model_validate(resp.json())
156
+ return GetPageContentResult(success=True, page=page)
157
+
158
+
159
+ @tool(
160
+ scopes=SCOPES["atlassian_confluence_create_page"],
161
+ api_docs="https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-wiki-api-v2-pages-post",
162
+ provider="atlassian",
163
+ service="atlassian_confluence",
164
+ )
165
+ async def atlassian_confluence_create_page(
166
+ params: CreatePageParams,
167
+ *,
168
+ token: str,
169
+ base_url: str = _BASE_URL,
170
+ ) -> CreatePageResult:
171
+ """Create a new Confluence page."""
172
+ cloud_id = await _resolve_cloud_id(token, base_url)
173
+ if not cloud_id:
174
+ return CreatePageResult(
175
+ success=False,
176
+ error="Failed to resolve Confluence cloud ID. Ensure you have access to a Confluence site.",
177
+ )
178
+
179
+ payload: dict = {
180
+ "spaceId": params.space_id,
181
+ "status": params.status,
182
+ "title": params.title,
183
+ "body": {
184
+ "representation": "storage",
185
+ "value": params.body,
186
+ },
187
+ }
188
+ if params.parent_id:
189
+ payload["parentId"] = params.parent_id
190
+
191
+ try:
192
+ async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
193
+ resp = await client.post(
194
+ _api_v2_url(cloud_id, "/pages", base_url=base_url),
195
+ headers=_headers(token, content_type=True),
196
+ json=payload,
197
+ )
198
+ except httpx.HTTPError as exc:
199
+ return CreatePageResult(success=False, error=str(exc))
200
+
201
+ if not resp.is_success:
202
+ return CreatePageResult(
203
+ success=False,
204
+ error=f"Confluence API error {resp.status_code}: {resp.text}",
205
+ )
206
+
207
+ page = PageSummary.model_validate(resp.json())
208
+ return CreatePageResult(success=True, page=page)
209
+
210
+
211
+ @tool(
212
+ scopes=SCOPES["atlassian_confluence_update_page"],
213
+ api_docs="https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-wiki-api-v2-pages-id-get",
214
+ provider="atlassian",
215
+ service="atlassian_confluence",
216
+ )
217
+ async def atlassian_confluence_update_page(
218
+ params: UpdatePageParams,
219
+ *,
220
+ token: str,
221
+ base_url: str = _BASE_URL,
222
+ ) -> UpdatePageResult:
223
+ """Update an existing Confluence page.
224
+
225
+ Fetches the current version number first, then sends the update with
226
+ version incremented by one.
227
+ """
228
+ cloud_id = await _resolve_cloud_id(token, base_url)
229
+ if not cloud_id:
230
+ return UpdatePageResult(
231
+ success=False,
232
+ error="Failed to resolve Confluence cloud ID. Ensure you have access to a Confluence site.",
233
+ )
234
+
235
+ # Fetch current page to get the version number.
236
+ page_url = _api_v2_url(cloud_id, f"/pages/{params.page_id}", base_url=base_url)
237
+ try:
238
+ async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
239
+ get_resp = await client.get(
240
+ page_url,
241
+ headers=_headers(token),
242
+ )
243
+ except httpx.HTTPError as exc:
244
+ return UpdatePageResult(success=False, error=str(exc))
245
+
246
+ if not get_resp.is_success:
247
+ return UpdatePageResult(
248
+ success=False,
249
+ error=f"Confluence API error {get_resp.status_code}: {get_resp.text}",
250
+ )
251
+
252
+ current_data = get_resp.json()
253
+ current_version = current_data.get("version", {}).get("number", 0)
254
+
255
+ payload = {
256
+ "id": params.page_id,
257
+ "status": params.status,
258
+ "title": params.title,
259
+ "body": {
260
+ "representation": "storage",
261
+ "value": params.body,
262
+ },
263
+ "version": {
264
+ "number": current_version + 1,
265
+ "message": "",
266
+ },
267
+ }
268
+
269
+ try:
270
+ async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
271
+ put_resp = await client.put(
272
+ page_url,
273
+ headers=_headers(token, content_type=True),
274
+ json=payload,
275
+ )
276
+ except httpx.HTTPError as exc:
277
+ return UpdatePageResult(success=False, error=str(exc))
278
+
279
+ if not put_resp.is_success:
280
+ return UpdatePageResult(
281
+ success=False,
282
+ error=f"Confluence API error {put_resp.status_code}: {put_resp.text}",
283
+ )
284
+
285
+ page = PageSummary.model_validate(put_resp.json())
286
+ return UpdatePageResult(success=True, page=page)
287
+
288
+
289
+ @tool(
290
+ scopes=SCOPES["atlassian_confluence_search_content"],
291
+ api_docs="https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-search/#api-wiki-rest-api-search-get",
292
+ provider="atlassian",
293
+ service="atlassian_confluence",
294
+ )
295
+ async def atlassian_confluence_search_content(
296
+ params: SearchContentParams,
297
+ *,
298
+ token: str,
299
+ base_url: str = _BASE_URL,
300
+ ) -> SearchContentResult:
301
+ """Search Confluence content using CQL (Confluence Query Language)."""
302
+ cloud_id = await _resolve_cloud_id(token, base_url)
303
+ if not cloud_id:
304
+ return SearchContentResult(
305
+ success=False,
306
+ error="Failed to resolve Confluence cloud ID. Ensure you have access to a Confluence site.",
307
+ )
308
+
309
+ try:
310
+ async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
311
+ resp = await client.get(
312
+ _api_v1_url(cloud_id, "/search", base_url=base_url),
313
+ headers=_headers(token),
314
+ params={"cql": params.cql, "limit": params.limit},
315
+ )
316
+ except httpx.HTTPError as exc:
317
+ return SearchContentResult(success=False, error=str(exc))
318
+
319
+ if not resp.is_success:
320
+ return SearchContentResult(
321
+ success=False,
322
+ error=f"Confluence API error {resp.status_code}: {resp.text}",
323
+ )
324
+
325
+ data = resp.json()
326
+ results = [SearchResult.model_validate(r) for r in data.get("results", [])]
327
+ return SearchContentResult(
328
+ success=True,
329
+ results=results,
330
+ total_size=data.get("totalSize", len(results)),
331
+ cql_query=data.get("cqlQuery", params.cql),
332
+ )
333
+
334
+
335
+ @tool(
336
+ scopes=SCOPES["atlassian_confluence_get_child_pages"],
337
+ api_docs="https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-children/#api-pages-id-direct-children-get",
338
+ provider="atlassian",
339
+ service="atlassian_confluence",
340
+ )
341
+ async def atlassian_confluence_get_child_pages(
342
+ params: GetChildPagesParams,
343
+ *,
344
+ token: str,
345
+ base_url: str = _BASE_URL,
346
+ ) -> GetChildPagesResult:
347
+ """List direct child pages of a Confluence page."""
348
+ cloud_id = await _resolve_cloud_id(token, base_url)
349
+ if not cloud_id:
350
+ return GetChildPagesResult(
351
+ success=False,
352
+ error="Failed to resolve Confluence cloud ID. Ensure you have access to a Confluence site.",
353
+ )
354
+
355
+ try:
356
+ async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
357
+ resp = await client.get(
358
+ _api_v2_url(
359
+ cloud_id,
360
+ f"/pages/{params.page_id}/direct-children",
361
+ base_url=base_url,
362
+ ),
363
+ headers=_headers(token),
364
+ )
365
+ except httpx.HTTPError as exc:
366
+ return GetChildPagesResult(success=False, error=str(exc))
367
+
368
+ if not resp.is_success:
369
+ return GetChildPagesResult(
370
+ success=False,
371
+ error=f"Confluence API error {resp.status_code}: {resp.text}",
372
+ )
373
+
374
+ data = resp.json()
375
+ children = [ChildPageSummary.model_validate(c) for c in data.get("results", [])]
376
+ return GetChildPagesResult(success=True, children=children)
377
+
378
+
379
+ @tool(
380
+ scopes=SCOPES["atlassian_confluence_upload_attachment"],
381
+ api_docs="https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-content-attachments/",
382
+ provider="atlassian",
383
+ service="atlassian_confluence",
384
+ )
385
+ async def atlassian_confluence_upload_attachment(
386
+ params: UploadAttachmentParams,
387
+ *,
388
+ token: str,
389
+ base_url: str = _BASE_URL,
390
+ ) -> UploadAttachmentResult:
391
+ """Upload a file as an attachment to a Confluence page."""
392
+ try:
393
+ data, filename, mime_type = await resolve_file_input(params.file)
394
+ except Exception as exc:
395
+ return UploadAttachmentResult(success=False, error=f"Failed to resolve file: {exc}")
396
+
397
+ cloud_id = await _resolve_cloud_id(token, base_url)
398
+ if not cloud_id:
399
+ return UploadAttachmentResult(
400
+ success=False,
401
+ error="Failed to resolve Confluence cloud ID. Ensure you have access to a Confluence site.",
402
+ )
403
+
404
+ url = _api_v1_url(cloud_id, f"/content/{params.page_id}/child/attachment", base_url=base_url)
405
+ headers = _headers(token)
406
+ headers["X-Atlassian-Token"] = "no-check"
407
+
408
+ try:
409
+ async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
410
+ resp = await client.post(
411
+ url,
412
+ headers=headers,
413
+ files={"file": (filename, data, mime_type)},
414
+ )
415
+ except httpx.HTTPError as exc:
416
+ return UploadAttachmentResult(success=False, error=str(exc))
417
+
418
+ if not resp.is_success:
419
+ return UploadAttachmentResult(
420
+ success=False,
421
+ error=f"Confluence API error {resp.status_code}: {resp.text}",
422
+ )
423
+
424
+ results = resp.json().get("results", [])
425
+ attachment_id = results[0].get("id", "") if results else ""
426
+ att_filename = results[0].get("title", filename) if results else filename
427
+
428
+ return UploadAttachmentResult(
429
+ success=True,
430
+ attachment_id=attachment_id,
431
+ filename=att_filename,
432
+ page_id=params.page_id,
433
+ )