Confluence (user passthrough)
Proxy for the Atlassian Confluence Cloud REST API via the managed gateway.
What this connector does
Gives agents gated, per-user access to Confluence for:
- Searching content with CQL.
- Reading pages (with body), their children, labels, attachments, and version history.
- Listing spaces and the pages within them.
- Reading, posting, editing, and deleting page comments — both footer and inline.
- Creating, updating, moving, and deleting pages.
- Creating and reading blog posts.
- Creating, reading, and deleting folders.
- Adding and removing labels.
The full CRUD lifecycle is covered: reads are readOnlyHint (allow-listable),
the three irreversible deletes (delete_page, delete_folder, delete_comment)
carry anthropic/requiresUserInteraction so a blanket approval cannot cover them,
and destructive writes (update_page, move_page, remove_label) are marked
destructiveHint. See the connector-metadata-review skill for the reasoning.
All operations are gated by PBAC policy. Every call flows: agent → gateway →
/introspect (with this connector's resource_type) → OPA decision →
Confluence API → response.
This connector ships in idp_passthrough mode only: the gateway forwards
each calling user's own Atlassian OAuth token (captured when they log in to
PolicyArc through the Atlassian IdP), as Authorization: Bearer. Confluence
enforces its own space and page permissions, and its audit shows the real
person. There is no shared service-account mode — Atlassian Cloud exposes no
service identity at the OAuth layer (see the Atlassian products page).
API surface
Content and spaces use the Confluence v2 API (/wiki/api/v2/*), Atlassian's
current REST API. Two operations fall back to the v1 endpoints because v2 has
no equivalent: CQL search (/wiki/rest/api/search) and label add/remove
and page move (/wiki/rest/api/content/{id}/...).
Because the v2 API is used, the connector requests granular Atlassian OAuth
scopes — not the classic read:confluence-content.all family, which the v2
API rejects with HTTP 401 "scope does not match". Grant these on the Atlassian
3LO app's Confluence API permission:
- Pages:
read:page:confluence,write:page:confluence,delete:page:confluence - Blog posts:
read:blogpost:confluence,write:blogpost:confluence - Folders:
read:folder:confluence,write:folder:confluence,delete:folder:confluence - Comments:
read:comment:confluence,write:comment:confluence,delete:comment:confluence - Labels:
read:label:confluence,write:label:confluence - Attachments:
read:attachment:confluence; plus the classicwrite:confluence-fileforadd_attachment— the v1 upload endpoint rejects the granularwrite:attachment:confluencewith 401"scope does not match". This one classic scope coexists fine with the granular set on a single token. - Spaces:
read:space:confluence - Plus
search:confluence,read:me, andoffline_access.
The internal PBAC scopes an agent's token must carry are coarser:
confluence:content:read / :write / :delete, confluence:space:read, and
confluence:comments:read / :write. Destructive page and folder deletes sit on
the dedicated confluence:content:delete scope so policy can withhold deletion
without withholding writes.
Two Confluence quirks the tools document inline:
- Body representation. Page and comment bodies use the
storagerepresentation (Confluence XHTML), e.g.{"representation": "storage", "value": "<p>Hello</p>"}— not plain text. - Optimistic locking on update.
update_pagerequires the next version number. Callget_pagefirst, readversion.number, then passversion: {"number": N+1}.
Prerequisites
- A Confluence Cloud account with access to the spaces/pages you want to reach.
- An Atlassian IdP registered in PolicyArc (provider key
atlassian). Pick Atlassian in the IdP setup wizard — the bundled preset fills in the endpoints and the requiredaudience=api.atlassian.comauthorize param. You supply the Client ID / secret from an Atlassian OAuth 2.0 (3LO) app (developer.atlassian.com) whose callback URL is the AS's/oauth2/callback, with the Confluence API permission granted for the connector's granular scopes (see above). - Your site's cloud ID — get it from
https://<site>.atlassian.net/_edge/tenant_info.
See Atlassian products — multi-connector setup for the full 3LO walkthrough (shared with the Jira connector).
Install
Provide two setup fields: Atlassian site (the subdomain, e.g. acme) and
Atlassian cloud ID (the UUID). Passthrough installs need no secret. On
install, the gateway merges this connector's upstream_scopes into the
Atlassian IdP row, so users re-consent to the union on their next login.
Verify
Request a token with a Confluence scope and call through the gateway as a
logged-in user (passthrough needs a user session, so client_credentials will
return a 502 "no stored IdP token"):
curl "https://<your-as>/gateway/confluence/wiki/api/v2/spaces" \
-H "Authorization: Bearer <user-token>"
A 200 with the spaces JSON confirms: token → introspect allowed → gateway proxied → Confluence responded as the user.
What can go wrong
| Symptom | Cause | Fix |
|---|---|---|
502 "no stored IdP token" | Caller never logged in through the atlassian IdP (or used client_credentials, which has no user context) | Re-authenticate via Atlassian. |
403 from Confluence | The user lacks permission on the space/page, or the token is missing a scope | Check the user's Confluence permissions and that the Atlassian app granted the scope. |
400 on create/update | Body sent as plain text instead of the storage representation | Wrap content as {"representation": "storage", "value": "<p>...</p>"}. |
409 on update | Stale version.number | Re-read with get_page and pass version.number + 1. |
403 from the gateway | Agent lacks the route's PBAC scope | Request a token with the matching confluence:* scope. |
Stored IdP token missing required scopes | Connector installed after the user logged in | User signs out and back in to re-consent. |
Reference
- Manifest:
connectors/identos.confluence/connector.v1.jsonin the repo. - IdP preset:
src/main/resources/idp-presets/atlassian.json. - Multi-connector setup: Atlassian products.
- Upstream API docs: Confluence Cloud v2 REST API.
Manifest reference
- ID:
identos.confluence - Version:
1.2.0 - Resource type:
urn:connector:identos:confluence - Capabilities:
mcp
Supported auth modes
| Type | Details |
|---|---|
idp_passthrough | requires IdP atlassian; setup fields: site, cloudid |
Setup fields
| ID | Label | Default | Secret? | Notes |
|---|---|---|---|---|
site | Atlassian site | — | no | placeholder: acme / The subdomain of your Atlassian cloud URL (e.g. 'acme' for acme.atlassian.net). Used in resource IDs and to locate the cloud ID. |
cloudid | Atlassian cloud ID | — | no | placeholder: 11223344-5566-7788-9900-aabbccddeeff / UUID identifying your Atlassian site. Get from https://{site}.atlassian.net/_edge/tenant_info (the cloudId field). Required because Atlassian Cloud OAuth tokens route through https://api.atlassian.com/ex/confluence/\{cloudid\}/, not the site URL directly. |
Scopes
| Scope |
|---|
confluence:content:read |
confluence:content:write |
confluence:content:delete |
confluence:space:read |
confluence:comments:read |
confluence:comments:write |
Routes
| Method | Pattern | Scope | Resource template |
|---|---|---|---|
GET | /wiki/rest/api/search | confluence:content:read | — |
GET | /wiki/api/v2/pages/{id} | confluence:content:read | confluence://{{site}}/pages/{{id}} |
GET | /wiki/api/v2/pages | confluence:content:read | — |
GET | /wiki/api/v2/pages/{id}/children | confluence:content:read | confluence://{{site}}/pages/{{id}} |
GET | /wiki/api/v2/pages/{id}/labels | confluence:content:read | confluence://{{site}}/pages/{{id}} |
POST | /wiki/rest/api/content/{id}/label | confluence:content:write | confluence://{{site}}/pages/{{id}} |
DELETE | /wiki/rest/api/content/{id}/label | confluence:content:write | confluence://{{site}}/pages/{{id}} |
GET | /wiki/api/v2/pages/{id}/attachments | confluence:content:read | confluence://{{site}}/pages/{{id}} |
GET | /wiki/api/v2/attachments/{id} | confluence:content:read | confluence://{{site}}/attachments/{{id}} |
POST | /wiki/rest/api/content/{id}/child/attachment | confluence:content:write | confluence://{{site}}/pages/{{id}} |
GET | /wiki/api/v2/spaces | confluence:space:read | — |
GET | /wiki/api/v2/spaces/{id} | confluence:space:read | confluence://{{site}}/spaces/{{id}} |
GET | /wiki/api/v2/spaces/{id}/pages | confluence:space:read | confluence://{{site}}/spaces/{{id}} |
GET | /wiki/api/v2/pages/{id}/footer-comments | confluence:comments:read | confluence://{{site}}/pages/{{id}} |
GET | /wiki/api/v2/pages/{id}/inline-comments | confluence:comments:read | confluence://{{site}}/pages/{{id}} |
GET | /wiki/api/v2/footer-comments/{id} | confluence:comments:read | confluence://{{site}}/comments/{{id}} |
GET | /wiki/api/v2/pages/{id}/versions | confluence:content:read | confluence://{{site}}/pages/{{id}} |
GET | /wiki/api/v2/folders/{id} | confluence:content:read | confluence://{{site}}/folders/{{id}} |
GET | /wiki/api/v2/blogposts | confluence:content:read | — |
GET | /wiki/api/v2/blogposts/{id} | confluence:content:read | confluence://{{site}}/blogposts/{{id}} |
POST | /wiki/api/v2/pages | confluence:content:write | — |
POST | /wiki/api/v2/blogposts | confluence:content:write | — |
POST | /wiki/api/v2/folders | confluence:content:write | — |
PUT | /wiki/api/v2/pages/{id} | confluence:content:write | confluence://{{site}}/pages/{{id}} |
PUT | /wiki/rest/api/content/{id}/move/{position}/{targetId} | confluence:content:write | confluence://{{site}}/pages/{{id}} |
PUT | /wiki/api/v2/footer-comments/{id} | confluence:comments:write | confluence://{{site}}/comments/{{id}} |
POST | /wiki/api/v2/footer-comments | confluence:comments:write | — |
POST | /wiki/api/v2/inline-comments | confluence:comments:write | — |
DELETE | /wiki/api/v2/pages/{id} | confluence:content:delete | confluence://{{site}}/pages/{{id}} |
DELETE | /wiki/api/v2/folders/{id} | confluence:content:delete | confluence://{{site}}/folders/{{id}} |
DELETE | /wiki/api/v2/footer-comments/{id} | confluence:comments:write | confluence://{{site}}/comments/{{id}} |
MCP tools
| Name | Scope | Description |
|---|---|---|
search_pages | confluence:content:read | Search Confluence content with CQL (Confluence Query Language). Example cql: "type=page AND space=DEV AND text ~ "onboarding"". Returns {results, _links:{next}, ...}; pass the opaque cursor from _links.next (or start) to page. This is Confluence's v1 search endpoint — the v2 API has no CQL equivalent. |
get_page | confluence:content:read | Get a single Confluence page by numeric id. By default the body is NOT included — pass body-format=storage (or atlas_doc_format) to get the content. The response carries version.number, which you MUST read and increment before calling update_page (optimistic locking). |
list_pages | confluence:content:read | List pages, optionally filtered by space id, title, or status. Paginated via the opaque cursor from _links.next. |
get_page_children | confluence:content:read | List the direct child pages of a Confluence page (one level). Paginated via cursor. |
list_page_labels | confluence:content:read | List the labels attached to a Confluence page. |
add_label | confluence:content:write | Add a label to a Confluence page (or other content) by id. Sends a single label object as the body; Confluence also accepts an array but this tool adds one at a time. This is the v1 labels endpoint — v2 has no label-write route. |
remove_label | confluence:content:write | Remove a label from a Confluence page by id. The label is passed as the name query parameter (this form tolerates '/' in label names). Reversible by re-adding, so it is not marked always-ask. |
list_page_attachments | confluence:content:read | List attachments on a Confluence page (metadata only — title, mediaType, fileSize, downloadLink). |
get_attachment | confluence:content:read | Get a single attachment's metadata by id (title, mediaType, fileSize, comment, version, and a downloadLink). Returns metadata, not the binary content. |
add_attachment | confluence:content:write | Attach a file to a Confluence page. Because MCP arguments are JSON, the file is supplied as base64 in file_content_base64 (plus a filename); the gateway assembles the multipart/form-data upload and adds Atlassian's required X-Atlassian-Token header. This is the v1 attachment endpoint (v2 has no attachment-create). Needs the classic write:confluence-file scope — the v1 endpoint rejects the granular write:attachment:confluence with 401 'scope does not match'. |
list_spaces | confluence:space:read | List Confluence spaces the caller can see. Use the returned numeric space id for list_pages/list_space_pages and the key (e.g. DEV) in CQL. Paginated via cursor. |
get_space | confluence:space:read | Get a single Confluence space by its numeric id. |
list_space_pages | confluence:space:read | List pages in a Confluence space. Paginated via cursor. |
list_page_comments | confluence:comments:read | List footer comments on a Confluence page. (Footer comments are the page-level comment thread; inline comments are list_inline_comments.) |
list_inline_comments | confluence:comments:read | List root-level inline comments on a Confluence page (comments anchored to a text selection, distinct from footer comments). Paginated via cursor. |
get_comment | confluence:comments:read | Get a single footer comment by its id, including its body and version.number (needed to update_comment). |
list_page_versions | confluence:content:read | List the version history of a Confluence page (each entry has number, authorId, createdAt, and the change message). Paginated via cursor. |
get_folder | confluence:content:read | Get a single Confluence folder by its numeric id (title, parentId, spaceId). Folders are a v2 content type that organise pages within a space. |
list_blog_posts | confluence:content:read | List blog posts, optionally filtered by space id, title, or status. Paginated via the opaque cursor from _links.next. |
get_blog_post | confluence:content:read | Get a single blog post by numeric id. By default the body is NOT included — pass body-format=storage (or atlas_doc_format) to get the content. |
create_page | confluence:content:write | Create a Confluence page. The body uses the storage representation (Confluence XHTML), NOT plain text. Minimal valid shape: {"spaceId": "123456", "status": "current", "title": "My page", "body": {"representation": "storage", "value": "<p>Hello</p>"}}. Get spaceId from list_spaces. To nest under a parent, add "parentId": "<pageId>". |
create_blog_post | confluence:content:write | Create a Confluence blog post. Same body caveat as create_page — the body uses the storage representation (Confluence XHTML). Shape: {"spaceId": "123456", "status": "current", "title": "My post", "body": {"representation": "storage", "value": "<p>Hello</p>"}}. |
create_folder | confluence:content:write | Create a folder in a Confluence space to organise pages. Shape: {"spaceId": "123456", "title": "My folder"}. To nest under an existing folder or page, add "parentId". Folders are a v2 content type; this needs the write:folder:confluence Atlassian scope. |
update_page | confluence:content:write | Update a Confluence page, overwriting its body. OPTIMISTIC LOCKING: call get_page first, read version.number, and pass version:{number: N+1}. The body uses the storage representation (same caveat as create_page). Required fields: id, status, title, body, version. Example: {"id": "123", "status": "current", "title": "My page", "body": {"representation": "storage", "value": "<p>Updated</p>"}, "version": {"number": 4, "message": "edit via agent"}}. |
move_page | confluence:content:write | Move a Confluence page relative to another page. position = 'append' makes the page a child of targetId; 'before'/'after' place it as a sibling immediately before/after targetId. Nothing is deleted — the page is relocated — but it overwrites the page's parent, so it is marked a write. This is the v1 content-move endpoint (v2 has no move). |
update_comment | confluence:comments:write | Update a footer comment, overwriting its body. OPTIMISTIC LOCKING like update_page: call get_comment first, read version.number, pass version:{number: N+1}. Body uses the storage representation. Shape: {"id": "123", "body": {"representation": "storage", "value": "<p>Edited.</p>"}, "version": {"number": 2}}. |
add_comment | confluence:comments:write | Add a footer comment to a Confluence page. Body uses the storage representation. Shape: {"pageId": "123", "body": {"representation": "storage", "value": "<p>Looks good.</p>"}}. To reply to an existing comment, add "parentCommentId". |
add_inline_comment | confluence:comments:write | Add an inline comment anchored to a text selection on a page. Shape: {"pageId": "123", "body": {"representation": "storage", "value": "<p>Question.</p>"}, "inlineCommentProperties": {"textSelection": "the exact highlighted text", "textSelectionMatchCount": 1, "textSelectionMatchIndex": 0}}. The textSelection must match text in the page body. |
delete_page | confluence:content:delete | Delete a Confluence page by id. By default this moves the page to the trash (restorable); it still removes the page from its space. Needs the delete:page:confluence Atlassian scope. |
delete_folder | confluence:content:delete | Delete a Confluence folder by id. Moves the folder to the trash; its contents go with it, so this can remove far more than one page. Needs the delete:folder:confluence Atlassian scope. |
delete_comment | confluence:comments:write | Delete a footer comment by id. This is permanent — comments are not trashed like pages. Kept on comments:write (not the content:delete scope) because it removes a single comment, not page content. |