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, and attachments.
- Listing spaces and the pages within them.
- Reading and posting page (footer) comments.
- Creating and updating pages.
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. CQL search uses the v1 endpoint (/wiki/rest/api/search)
because v2 has no CQL equivalent.
Because the v2 API is used, the connector requests granular Atlassian OAuth
scopes (read:page:confluence, write:page:confluence, read:space:confluence,
read:comment:confluence, write:comment:confluence, read:attachment:confluence,
read:label:confluence, search:confluence, plus read:me and offline_access)
— not the classic read:confluence-content.all family, which the v2 API
rejects with HTTP 401 "scope does not match". Grant these granular scopes on
the Atlassian 3LO app's Confluence API permission.
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.0.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: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}} |
GET | /wiki/api/v2/pages/{id}/attachments | confluence:content:read | 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}} |
POST | /wiki/api/v2/pages | confluence:content:write | — |
PUT | /wiki/api/v2/pages/{id} | confluence:content:write | confluence://{{site}}/pages/{{id}} |
POST | /wiki/api/v2/footer-comments | confluence:comments:write | — |
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. |
list_page_attachments | confluence:content:read | List attachments on a Confluence page (metadata only — title, mediaType, fileSize, downloadLink). |
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 a separate endpoint not exposed here.) |
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>". |
update_page | confluence:content:write | Update a Confluence page. 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"}}. |
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". |