About Netra Confluence MCP
Netra Confluence MCP is an MCP server published by sunishbharat in the Search category: this tool lets your AI assistant (Claude, GitHub Copilot, or any MCP-compatible client) read and edit Confluence pages for you. You describe what to change in plain language, and the tool will do it for you.
The repository has 1 stars and 0 forks, with the last commit 14 days ago. Recent commit activity is the strongest signal that reported issues will get attention.
Install
git clone https://github.com/sunishbharat/netra-confluence-mcpThis server has no ConduID identity, so agent calls to it are not receipted. Pin the version you install and review the source before granting it credentials.
Tools (5)
clone_release_reportClones a page, updates all version tokens and macro IDs
Parameters not published.
create_page_from_adfCreates a new page from an ADF body
Parameters not published.
inspect_page_jqlLists all Jira macro JQL queries on a page with location paths
Parameters not published.
update_page_macrosReplaces values in macro parameters on any page
Parameters not published.
update_release_versionLike above, also updates date node timestamps
Parameters not published.
Ask AI
Ask AI about Netra Confluence MCP
Powered by Claude · Grounded in docs
Security checks
Static checks derived from the repository 5 days ago. Heuristics on the source, not an audit.
- ✓README presentThe repository ships documentation.
- ✓License declaredUsable under a stated license.
- ✓Tests presentA test suite exists in the repository.
- ✓Dependencies pinnedA lockfile pins dependency versions.
- ✓No dynamic code executionSource scan found no eval, dynamic Function, or shell execution.
1 check has not been run yet.
Releases
README
Netra MCP Server - Confluence Write Tool
This tool lets your AI assistant (Claude, GitHub Copilot, or any MCP-compatible client) read and edit Confluence pages for you. You describe what to change in plain language, and the AI calls the right tool.
What it can do:
- Show you every JQL query on a Confluence page, extracted cleanly with its location
- Replace version tokens or JQL values across an entire page in one shot
- Preview every change before anything is written (dry-run is on by default)
- Clone a release report page to a new version with all macros updated
Before you start
You need:
- Python 3.12 or newer - check with
python --version - uv (Python package manager) - install from docs.astral.sh/uv
- A Confluence Cloud account
- A Confluence API token - create one at: Atlassian account -> Security -> API tokens -> Create API token
Setup (one time)
1. Clone the repository
git clone <repo-url>
cd netra-confluence-mcp
2. Install dependencies
uv sync
3. Create your configuration file
cp .env.example .env
Open .env and fill in your details:
CONFLUENCE_BASE_URL=https://your-org.atlassian.net
CONFLUENCE_API_TOKEN=your-api-token-here
CONFLUENCE_USER_EMAIL=your-email@example.com
CONFLUENCE_SITE_URL=https://your-org.atlassian.net
LOG_LEVEL=INFO
CONFLUENCE_BASE_URL and CONFLUENCE_SITE_URL are usually the same value.
4. Test that it starts
uv run python server.py
You should see the server start without errors. Press Ctrl+C to stop it - your AI client will start it automatically when needed.
Connect your AI client
Claude Desktop
-
Open your Claude Desktop config file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
- macOS:
-
Add this block (adjust the
cwdpath to where you cloned the repo):
{
"mcpServers": {
"netra-confluence-writer": {
"command": "uv",
"args": ["run", "python", "server.py"],
"cwd": "/absolute/path/to/netra-confluence-mcp"
}
}
}
- Restart Claude Desktop. The Confluence tools will appear in the tool list.
Claude Desktop over HTTP (mcp-remote bridge)
When the server runs in HTTP mode (Docker container, remote host, or SERVER_TRANSPORT=http uv run python server.py) instead of as a stdio child process, use mcp-remote as a stdio-to-HTTP bridge. Claude Desktop's JSON config only spawns stdio children, so the bridge translates between Claude Desktop and the running HTTP endpoint.
Install mcp-remote once:
npm install -g mcp-remote
Add to claude_desktop_config.json:
{
"mcpServers": {
"netra-confluence-writer": {
"command": "mcp-remote",
"args": ["http://127.0.0.1:8765/mcp"]
}
}
}
Prefer no global install? Use npx instead:
{
"mcpServers": {
"netra-confluence-writer": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://127.0.0.1:8765/mcp"]
}
}
}
The Confluence tools appear in the tool list once the bridge connects to the running server. The server must already be listening at the URL in args (see Production transport (http)).
VS Code with GitHub Copilot
-
Open your VS Code user settings (
Ctrl+Shift+P-> "Open User Settings JSON") -
Add:
{
"mcp": {
"servers": {
"netra-confluence-writer": {
"command": "uv",
"args": ["run", "python", "server.py"],
"cwd": "/absolute/path/to/netra-confluence-mcp"
}
}
}
}
- Reload VS Code. The tools are available in Copilot Chat when you click the tools icon.
VS Code with GitHub Copilot - shared server on CF
When the server is deployed on Cloud Foundry (see Docker and cloud deployment), Copilot connects to it over http and sends your own Atlassian credentials as headers. Nothing runs locally.
-
Open the MCP configuration:
Cmd+Shift+P/Ctrl+Shift+P-> "MCP: Open User Configuration". The file ismcp.json:- macOS:
~/Library/Application Support/Code/User/mcp.json - Windows:
%APPDATA%\Code\User\mcp.json - Or per project:
.vscode/mcp.jsonin the workspace
- macOS:
-
Add the server (replace the URL with your CF route, and the email and tokens with your own):
{
"servers": {
"netra-confluence-jira-mcp": {
"url": "https://<your-cf-route>/mcp",
"type": "http",
"headers": {
"X-Confluence-User-Email": "you@example.com",
"X-Confluence-Api-Token": "<your Atlassian API token>",
"X-Jira-User-Email": "you@example.com",
"X-Jira-Api-Token": "<your Atlassian API token>"
}
}
},
"inputs": []
}
- Start the server from Command Palette -> "MCP: List Servers" ->
netra-confluence-jira-mcp-> Start (or Restart after editing). The Confluence and Jira tools appear in Copilot Agent mode under the tools icon.
Option: same token for Confluence and Jira (two headers). An Atlassian API token works for Confluence and Jira on the same site. If both pairs above would hold the same email and token, drop the X-Jira-* lines; the Jira tools then use the X-Confluence-* pair:
{
"servers": {
"netra-confluence-jira-mcp": {
"url": "https://<your-cf-route>/mcp",
"type": "http",
"headers": {
"X-Confluence-User-Email": "you@example.com",
"X-Confluence-Api-Token": "<your Atlassian API token>"
}
}
},
"inputs": []
}
Remove both X-Jira-* lines or neither. With only one of them present, Jira calls fail rather than mixing a Jira email with the Confluence token. Use the four-header version only when Jira needs a different token.
Notes:
- The URL must end in
/mcp. The root URL of the route is not the MCP endpoint. - Keep the token out of shared files. This file holds a live API token in plain text. Never commit a
.vscode/mcp.jsonthat contains one. To have VS Code ask for the token once and keep it in its encrypted secret storage instead, use an input:
{
"inputs": [
{"type": "promptString", "id": "atl-token", "description": "Atlassian API token", "password": true}
],
"servers": {
"netra-confluence-jira-mcp": {
"url": "https://<your-cf-route>/mcp",
"type": "http",
"headers": {
"X-Confluence-User-Email": "you@example.com",
"X-Confluence-Api-Token": "${input:atl-token}"
}
}
}
}
- Updating the token: edit the value in
mcp.jsonand restart the server from "MCP: List Servers". With an input, clear the stored value (Command Palette, type "MCP") and VS Code asks for the new token on the next start. - Credential errors: a tool response of
{"status": "ERROR", "error": "missing per-user ... credentials"}means the headers did not reach the server; check the header names and that the server was restarted after editing.
Other MCP clients
Use uv run python server.py as the server command with the project directory as the working directory. By default the server uses stdio transport (a child process spawned per client) - the standard transport for local MCP clients.
Production transport (http)
For cloud or shared deployment the server runs standalone as a web service instead of being spawned as a child process - one server process serves many clients over HTTP. Every tool call here is a single, independent read-transform-write against Confluence (there is no clarification loop or multi-turn session), so the server runs stateless: no session store, no Valkey, nothing to keep in memory between requests.
Start it in HTTP mode:
SERVER_TRANSPORT=http uv run python server.py
(Or set SERVER_TRANSPORT=http in .env instead.) main() in server.py reads the settings and passes host, port, and stateless_http=True directly to FastMCP's run() for this transport, starting uvicorn bound to 127.0.0.1:8765 by default. The /mcp path is FastMCP's default endpoint for the streamable HTTP protocol:
http://127.0.0.1:8765/mcp
To change the bind address or port, set SERVER_HOST / SERVER_PORT before starting (e.g. host 0.0.0.0 to accept connections from other machines). A /health endpoint is also exposed for load balancer / CF health checks:
curl http://127.0.0.1:8765/health # {"status":"ok"}
Connect with the MCP Inspector (npx @modelcontextprotocol/inspector, transport "Streamable HTTP", URL http://127.0.0.1:8765/mcp), register it with Claude Code:
claude mcp add --transport http netra-confluence http://127.0.0.1:8765/mcp
or use mcp-remote as a stdio-to-HTTP bridge from Claude Desktop (see Claude Desktop over HTTP).
No endpoint auth yet. There is no API-key gate on the /mcp endpoint itself (see Phase 4 in docs/netra-mcp-confluence-write-phased-design.md). Keep the default loopback bind (127.0.0.1) unless you are on a trusted network or sitting behind an auth-terminating proxy - see "Team access on http (multi-user credential passthrough)" below for why this matters even after per-user credentials are wired up.
Team access on http (multi-user credential passthrough)
On http transport the server holds no Confluence identity of its own - CONFLUENCE_API_TOKEN / CONFLUENCE_USER_EMAIL must be left unset in the server's environment. Every tool call instead carries the caller's own identity as request headers:
X-Confluence-User-Email: alice@example.com
X-Confluence-Api-Token: <alice's own API token>
The server builds a fresh Confluence client from those headers per call. Two teammates hitting the same shared server therefore write as themselves - Confluence page history shows "Alice" and "Bob", not a shared service account - and each person is bound by their own Confluence permissions. A request with no headers, or a blank header, gets {"status": "ERROR", "error": "missing per-user Confluence credentials"} - there is no fallback to a shared identity.
Each team member's Claude Desktop config carries their own headers via mcp-remote --header:
{
"mcpServers": {
"netra-confluence-writer": {
"command": "npx",
"args": [
"-y", "mcp-remote", "https://netra-confluence.<your-domain>/mcp",
"--header", "X-Confluence-User-Email: alice@example.com",
"--header", "X-Confluence-Api-Token: ${CONFLUENCE_API_TOKEN}"
],
"env": { "CONFLUENCE_API_TOKEN": "<alice's own token>" }
}
}
}
(The env indirection keeps the raw token out of the JSON config body itself. Clients with native HTTP-header support can configure the two headers directly instead of going through mcp-remote.)
Requirements for running this in production, not just locally:
- TLS end to end. These headers carry a live API token; the route must be HTTPS-only. A plain-HTTP bind is only acceptable on
127.0.0.1. - The endpoint still needs its own gate. Header passthrough authenticates the Confluence call, not the MCP endpoint - anyone who can reach
/mcpand supply any valid Atlassian token can use the server as an open Confluence proxy. Keep the route internal-only, or put an API-key/mTLS gate in front of it at the router, until OAuth 2.0 (Tier 2, seedocs/netra-mcp-confluence-write-phased-design.mdsection 16.4) lands. - Don't log the headers.
X-Confluence-Api-Tokenmust never end up in request logs. A structlog processor redacts any log field whose key matchestoken/password/api_key/authorization(case-insensitive) to[REDACTED]as a backstop, but no code should pass credential values to a log call in the first place.
Jira tools on http. The Jira tools resolve credentials from the same request: X-Jira-User-Email / X-Jira-Api-Token if present, otherwise the X-Confluence-* pair (one Atlassian API token works for both products on the same site). Add the Jira pair only if you want Jira calls to use a different token:
"--header", "X-Jira-User-Email: alice@example.com",
"--header", "X-Jira-Api-Token: ${JIRA_API_TOKEN}"
The two pairs are never mixed, and env credentials are never used on http.
If you don't need a shared server at all, per-user stdio (each teammate runs their own local process with their own .env - see Setup) already gives native per-user attribution today with zero extra configuration.
Finding your Confluence page ID
The page ID is in the URL when you open a Confluence page:
https://your-org.atlassian.net/wiki/spaces/MYSPACE/pages/1234567890/Page+Title
^^^^^^^^^^
this is your page ID
Recipe: list all JQL queries on a page
Before you decide what to change, you can ask the AI to just show you what is on the page. This is read-only - nothing is written.
"Show me all the JQL queries on Confluence page 34334."
The AI calls inspect_page_jql("34334") and returns:
{
"status": "INSPECTION",
"page_id": "34334",
"title": "R1.0 Release Report ProjectX",
"jira_macro_count": 7,
"jql_queries": [
{
"macro_id": "abc-123-...",
"location_path": "table[0]/row[1]/cell[0]",
"jql": "project = MYPROJ AND \"release status app1\" in (V1.0, V2.0) AND fixVersion = R1.0",
"columns": "type,key,summary,status",
"server": "MyJira",
"max_issues": "20"
},
{
"macro_id": "def-456-...",
"location_path": "table[1]/row[0]/cell[0]",
"jql": "project = MYPROJ AND label = app1_R1.0_Baseline AND fixVersion = R1.0",
"columns": "type,key,summary",
"server": "MyJira",
"max_issues": "20"
}
],
"unique_strings": ["MYPROJ", "R1.0", "V1.0", "V2.0", "app1_R1.0", "app1_R1.0_Baseline"]
}
Key fields in the response:
status: "INSPECTION"- always present; confirms this is the read-only path. No write happened.jira_macro_count- how many Jira macros the page has. If this is0, the page has no Jira macros and there is nothing to replace.jql_queries[].jql- the exact JQL string stored in each macro. Copy this verbatim if you plan to write a replacement rule - whitespace and quoting must match exactly.jql_queries[].location_path- human-readable ADF path so you know which macro you are looking at (table[0]/row[1]/cell[0]).jql_queries[].macro_id- the macro's UUID. Preserved on update, regenerated on clone.unique_strings- deduplicated token candidates already split on JQL syntax (whitespace, operators, quotes). Useful as a starting point to spot candidates, but not a direct replacement rule - it is a list of candidates, not the full substrings you would replace.
When to use this recipe:
- Before any update, to see what release tokens, project names, or version labels appear on the page.
- After an update, to verify the change landed where you expected.
- When you are handed an unfamiliar page and need to know what is on it before deciding what to do.
You can also pass a full Confluence URL instead of a bare page ID - the AI will extract the ID:
"Show me the JQL on https://your-org.atlassian.net/wiki/spaces/ENG/pages/34334/R1.0-Report"
Controlling the output format
The tool always returns the full JSON shape (with status, page_id, title, jira_macro_count, jql_queries, and unique_strings). The agent has the full response in its context but can choose to show you only the JQL strings. So:
| Your prompt | What the agent shows you |
|---|---|
| "Show me all the JQL queries on page 34334." | The full JSON dump (default behavior, what we documented in the recipe above) |
| "List just the JQL strings on page 34334." | A plain list of only the jql field values |
| "Show me the JQL queries on page 34334 as a bullet list." | One bullet per JQL, no metadata |
| "Show me only the JQLs and nothing else from page 34334." | Same as above, with explicit instruction to suppress the rest |
| "Give me a markdown table of the JQL queries on page 34334." | A formatted table - the agent can decide which columns to include |
A competent MCP client (Claude, Rovo) will honor all of these. The user always sees what they asked for.
Worked example: change version V10.0 to V20.0
Say you have a release report page at page ID 2811939182 and every JQL query on it references V10.0. Here is how to update the whole page in three steps.
Step 1 - Inspect the page
Ask your AI:
"What JQL queries are on Confluence page 2811939182?"
The AI calls inspect_page_jql and returns something like:
{
"status": "INSPECTION",
"page_id": "2811939182",
"title": "V10.0 Release Report",
"jira_macro_count": 12,
"jql_queries": [
{
"macro_id": "abc-001",
"location_path": "table[0]/row[1]/cell[2]",
"jql": "project = MYPROJ AND fixVersion = ver_V10.0 AND status != Done",
"columns": "key,summary,status"
}
],
"unique_strings": ["Done", "MYPROJ", "ver_V10.0", "ver_V10.0_Baseline", "V10.0"]
}
Look at unique_strings - these are the values you can replace. You can see ver_V10.0_Baseline, ver_V10.0, and V10.0 all need to change.
Step 2 - Preview the changes (dry run)
Ask your AI:
"Preview replacing V10.0 with V20.0 on page 2811939182. Include label variants."
The AI calls update_release_version in dry-run mode:
{
"status": "DRY_RUN",
"current_title": "V10.0 Release Report",
"new_title": "V20.0 Release Report",
"current_version": 14,
"total_changes": 47,
"change_summary": "jql: 36 changes\ntext: 8 changes\ntitle: 3 changes",
"change_log": [...],
"message": "Preview only. Call again with dry_run=False to apply."
}
Nothing was written yet. You can see exactly what would change.
Step 3 - Apply the changes
If the preview looks correct, ask your AI:
"Apply it."
The AI calls the same tool with dry_run=False:
{
"status": "UPDATED",
"page_id": "2811939182",
"title": "V20.0 Release Report",
"version": 15,
"url": "https://your-org.atlassian.net/wiki/spaces/MYSPACE/pages/2811939182",
"total_changes": 47
}
Your Confluence page is now updated. Open the URL to verify.
Other things you can do
Replace a specific JQL value only
If you only want to change values inside JQL queries and not text or title:
"Replace OLD_VALUE with NEW_VALUE in JQL queries only on page 99887766."
This uses scope="jql" which is precise - it will not touch column header names or other macro parameters even if they contain the same string.
Clone a page to a new release
"Clone page 2811939182 from version V10.0 to V20.0 with delivery date 2026-09-15. Create it as a draft."
The AI calls clone_release_report. The clone gets:
- All version tokens updated
- All Jira macro UUIDs regenerated (so the clone has no ID collisions with the original)
- All date nodes updated to the new delivery date
Create a page from scratch
"Create a new Confluence page in space MYSPACE titled 'Project Plan' using this ADF body: {...}"
Clone a Jira template ticket (one or many)
"Create a clone of the Jira template ticket PROJ-124 and set the summary to ProjectX Variant-A Payment Service."
"Clone PROJ-124 once for each of these services: Payment, Billing, Ledger. Summary format: ProjectX Variant-A Service."
The AI expands the summary pattern into one exact summary per issue and calls clone_jira_issue. The dry run lists every issue it would create, which fields are copied from the template, and any summary that already exists in the project (those are skipped). Say "apply it" to create them. Each clone:
- Copies every template field the target project accepts (custom fields, labels, components, priority, description), but not assignee, reporter, attachments, links, or parent
- Gets an "is cloned by" link from the template
- Is attributed to you in Jira (you are the reporter)
Up to 50 issues per call. If some fail, the status is PARTIAL and each item says what went wrong. Re-running the same request only creates the missing ones.
Create or edit Jira issues
"Create 3 Tasks in project PROJ with summaries A, B, and C and label 'release-2'."
"Change the priority of PROJ-412 to High and add the label 'urgent'."
create_jira_issue works like the clone tool without a template. edit_jira_issue shows a before/after for every field it would change before writing.
Create Jira issues from an input file
Keep the ticket details in a file in your workspace and let the AI read it. The server never reads the file itself (on a shared http deployment it has no access to your machine): the AI client reads the file and passes its contents to create_jira_issue, or to clone_jira_issue if the file names a template.
In VS Code Copilot Agent mode, attach the file with #file: (or drag it into the chat):
"Create Jira tickets from #file:tickets/hadoop-rollout.md. Use the project, issue type, and fields given in the file. Show me the dry run first."
Check the preview, then say "create them". Any format the AI can read works (Markdown, YAML, JSON, CSV). Markdown with one section per ticket is the easiest to write by hand:
Project: PROJ
Issue type: Task
## Ticket: [Hadoop][vari-12] abc1 ingestion retries
Priority: High
Labels: hadoop, release-2
Components: Backend
Service Name: Payment
Description:
The ingestion job for abc1 fails on retries.
Add idempotent writes and an alert when retries exceed 3.
## Ticket: [Hadoop][vari-12] abc2 ingestion retries
Priority: Medium
Labels: hadoop
Service Name: Billing
Description:
Same fix for abc2.
Field names in the file, such as "Service Name", are matched to Jira fields by their display name. To create tickets from a template instead, put Clone template: PROJ-1285 at the top of the file and list one summary per ticket.
Things to know:
- Up to 50 tickets per call. For a larger file, ask the AI to create them in batches of 50.
- Re-runs are safe. Tickets whose summary already exists in the project are skipped, so after fixing a failed item you can run the whole file again.
- Field values are converted by the AI. A value like
Priority: Highmust become Jira's API shape. A wrongly shaped value is not caught by the dry run; that ticket comes backFAILEDwith Jira's error message, the rest are created (PARTIAL), and you fix the file and re-run. - Descriptions are plain text. Blank lines become paragraphs and line breaks are kept, but Markdown bullets, bold, and headings are not converted to Jira formatting.
Read, search, and analyse Jira issues
"Show me PROJ-1285."
"Read PROJ-1285 including comments and give me feedback: is the description clear, are the acceptance criteria testable, and what is missing before a developer can start?"
"Find all issues in PROJ with Hadoop in the summary and show their status and Service Name."
get_jira_issue returns one issue in readable form: description as plain text, custom fields by display name (for example "Acceptance Criteria"), parent, sub-tasks, linked issues, and attachment file names. Ask for comments to get the 20 most recent. search_jira_issues runs a JQL query and returns up to 100 issues with key, summary, status, assignee, priority, and any extra fields you name. Both are read-only. The AI does the analysis itself; it cannot read attachment contents, and its feedback stays in the chat.
How it works
flowchart TD
subgraph Client["Your AI (Claude / Copilot)"]
A[Your message]
end
subgraph Server["Netra MCP Server"]
subgraph Tools["MCP Tools"]
T0[inspect_page_jql]
T1[update_page_macros]
T2[update_release_version]
T3[clone_release_report]
T4[create_page_from_adf]
end
subgraph Engine["ADF Engine"]
R[Find and replace]
V[Validate before write]
end
end
subgraph Confluence["Confluence Cloud"]
API[REST API]
end
A --> T0 & T1 & T2 & T3 & T4
T1 & T2 & T3 --> R --> V
T0 & V --> API
The server reads pages as ADF (Atlassian Document Format - the native JSON format Confluence uses internally). It finds JQL values at their exact JSON path, replaces them, validates the result, and writes back. No regex is used at any point.
Tool reference
| Tool | What it does | Writes? |
|---|---|---|
inspect_page_jql |
Lists all Jira macro JQL queries on a page with location paths | Never |
update_page_macros |
Replaces values in macro parameters on any page | Yes, if dry_run=False |
update_release_version |
Like above, also updates date node timestamps | Yes, if dry_run=False |
clone_release_report |
Clones a page, updates all version tokens and macro IDs | Yes, if dry_run=False |
create_page_from_adf |
Creates a new page from an ADF body | Yes, if dry_run=False |
clone_jira_issue |
Clones a Jira template issue into 1-50 new issues | Yes, if dry_run=False |
create_jira_issue |
Creates 1-50 new Jira issues without a template | Yes, if dry_run=False |
edit_jira_issue |
Edits fields on one Jira issue | Yes, if dry_run=False |
get_jira_issue |
Reads one Jira issue, optionally with its recent comments | Never |
search_jira_issues |
Finds Jira issues with a JQL query (up to 100) | Never |
All write tools default to dry_run=True. You must explicitly say "apply it" or pass dry_run=False to make any change.
Response status values
| Status | Meaning |
|---|---|
INSPECTION |
Read-only result (inspect_page_jql, get_jira_issue, search_jira_issues) |
DRY_RUN |
Preview only; nothing written; change_log present |
UPDATED |
Page updated; version and url present |
CREATED |
New page created; page_id and url present |
NO_CHANGES |
No occurrences of the search term were found |
VALIDATION_FAILED |
ADF structure check failed; write blocked; errors list present |
VERSION_CONFLICT |
Someone else edited the page at the same time; retry |
PARTIAL |
Bulk Jira call where some issues were created and some failed; results has one entry per item |
ERROR |
API or network error; error field has details |
Troubleshooting
"Missing required fields" or 401 error
Check your .env file. Make sure CONFLUENCE_USER_EMAIL matches the email on your Atlassian account, and CONFLUENCE_API_TOKEN is a valid token (not your password).
"Page not found" (404)
Double-check the page ID from the URL. The ID is the number in .../pages/1234567890/....
"Permission denied" (403) Your account needs at least View permission on the page to inspect it, and Edit permission to update it.
The server is not appearing in my AI client
Make sure the cwd path in your client config is the absolute path to the folder containing server.py. On Windows use forward slashes or escape backslashes.
"Version conflict" after applying Someone else saved the page between your inspect and your apply. Just repeat the operation - the server re-reads the latest version before writing.
Docker and cloud deployment
The server ships as a self-contained, stateless Docker image - no Valkey, no external datastore. Full runbook (CF first deploy, blue-green updates, env var reference): docs/docker_cf_deployment.md.
Local dev with docker-compose
# From repo root (never from docker/)
docker-compose up
curl http://localhost:8765/health # {"status":"ok"}
curl http://localhost:8765/mcp # MCP endpoint
Build the image locally (single arch, fast)
Requires Docker Desktop 4.x+ with buildx (shipped by default).
VERSION=dev bash scripts/docker-build-local.sh
docker run -p 8765:8765 --env-file .env -e SERVER_TRANSPORT=http -e SERVER_HOST=0.0.0.0 \
ghcr.io/sunishbharat/netra-confluence-mcp:dev
The -e flags matter: --env-file overrides the image's defaults, and a local .env with SERVER_HOST=127.0.0.1 would make the server unreachable through the published port. Run the build script from Git Bash on Windows.
Build and push multi-platform (amd64 + arm64) and deploy to CF
docker buildx create --name multiarch --driver docker-container --use # one-time
docker buildx inspect --bootstrap
export REGISTRY=ghcr.io/sunishbharat
./scripts/cf-deploy.sh # builds both platforms, pushes to GHCR, then cf push
Set the Atlassian site URLs once (they survive cf push/cf restage). The Confluence and Jira tools both use them:
cf set-env netra-confluence-mcp CONFLUENCE_BASE_URL https://your-org.atlassian.net
cf set-env netra-confluence-mcp CONFLUENCE_SITE_URL https://your-org.atlassian.net
cf restage netra-confluence-mcp
Do not set CONFLUENCE_API_TOKEN, CONFLUENCE_USER_EMAIL, JIRA_USER_EMAIL, or JIRA_API_TOKEN on CF. On http they are ignored: each user's MCP client sends their own credentials as headers (see VS Code with GitHub Copilot - shared server on CF).
CI builds and pushes a multi-arch image to GHCR automatically on every v*.*.* tag push (.github/workflows/docker.yml).
Running tests
Unit tests (no Confluence connection required):
uv run python -m pytest
Integration tests against a real Confluence page:
CONFLUENCE_TEST_PAGE_ID=<page-id> uv run python -m pytest -m integration
Testing the Jira tools
The Jira tools (clone_jira_issue, create_jira_issue, edit_jira_issue, get_jira_issue, search_jira_issues) can be tested in three layers: unit tests with no Jira connection, direct tool calls through the MCP Inspector, and end-to-end prompts from your AI client. Do the layers in that order.
Use a sandbox Jira project. Every call with
dry_run=Falsecreates or edits real issues. Never point the apply steps below at a production project.
1. Unit tests (no Jira connection)
All Jira HTTP calls are mocked, so these run anywhere:
uv run python -m pytest tests/test_jira_*.py
| File | What it covers |
|---|---|
tests/test_jira_client.py |
Auth header, HTTP status mapping, 429 retry honoring Retry-After, no retry of a POST/PUT whose response was lost |
tests/test_jira_api.py |
Each REST call, bulk-create partial-failure parsing, createmeta pagination, exact-match duplicate search |
tests/test_jira_fields.py |
Field copying and normalization, override key resolution, required-field check, JQL escaping, plain text to ADF, input validation |
tests/test_jira_shared.py |
Credential resolution on stdio and http, CREATED / PARTIAL / ERROR / NO_CHANGES mapping, skip-existing, link failures |
tests/test_jira_tools.py |
The three write tools: dry run never writes, the payload sent on apply, validation blocks writes |
tests/test_jira_read_tools.py |
Readable formatting, get_jira_issue with and without comments, search_jira_issues columns, extra fields, truncation, errors |
With coverage:
uv run python -m pytest tests/test_jira_*.py --cov=netra_jira --cov-report=term-missing
2. Prepare a sandbox project
You need:
- A Jira project you can freely create issues in (for example
SANDBOX) - A template issue in it that looks like a real template: a few labels, a component, a priority, a description, and ideally one required custom field
- Jira permissions on that project: Browse projects, Create issues, Edit issues, and Link issues
Credentials: on stdio the Jira tools use JIRA_USER_EMAIL / JIRA_API_TOKEN from .env if both are set, otherwise your CONFLUENCE_USER_EMAIL / CONFLUENCE_API_TOKEN. One Atlassian API token works for both products, so usually nothing extra is needed.
3. Direct tool calls with the MCP Inspector
The Inspector calls a tool with exact arguments, with no AI in between, so you can test each case deterministically.
npx @modelcontextprotocol/inspector uv run python server.py
Open the URL it prints, go to Tools, pick a tool, fill in the arguments, and run it. Work through these cases in order, replacing SANDBOX-1 with your template key:
| # | Tool and arguments | Expected result |
|---|---|---|
| 1 | clone_jira_issue, template_issue_key="SANDBOX-1", items=[{"summary": "Test ProjectX Variant-A Payment Service"}] |
DRY_RUN, to_create: 1, copied_fields lists the template's labels, components, priority, description, and custom fields |
| 2 | Same as 1 with dry_run=false |
CREATED, one issue_key and url |
| 3 | Same as 2 again | NO_CHANGES, the item shows SKIPPED_EXISTS with the key from step 2. No duplicate is created |
| 4 | items with three summaries (...Payment..., ...Billing..., ...Ledger...), dry_run=false |
CREATED for the two new ones, SKIPPED_EXISTS for Payment |
| 5 | One item with "field_overrides": {"labels": ["override-test"]}, dry run |
DRY_RUN, that item lists "overrides": ["labels"] |
| 6 | One item with "field_overrides": {"No Such Field": 1} |
VALIDATION_FAILED naming the unknown field. Nothing is created |
| 7 | Two items, one with an invalid value for a select field (for example {"priority": {"name": "NotAPriority"}}), dry_run=false |
PARTIAL: one item CREATED, the other FAILED with Jira's error message |
| 8 | target_project_key set to a second sandbox project, dry run |
DRY_RUN, components and versions appear in dropped_fields |
| 9 | create_jira_issue, project_key="SANDBOX", issue_type="Task", items=[{"summary": "Test create", "description": "line one\n\nline two"}], dry_run=false |
CREATED; the description shows two paragraphs in Jira |
| 10 | edit_jira_issue, issue_key from step 9, summary="Test create (edited)", field_overrides={"labels": ["edited"]} |
DRY_RUN with a before / after entry for each field |
| 11 | Same as 10 with dry_run=false |
UPDATED, fields_updated: ["labels", "summary"] |
| 12 | Same as 11 again | NO_CHANGES |
| 13 | clone_jira_issue with template_issue_key="SANDBOX-99999" |
ERROR with a not-found message |
| 14 | get_jira_issue, issue_key="SANDBOX-1", include_comments=true |
INSPECTION; custom fields appear under their display names, and comments lists the newest first |
| 15 | search_jira_issues, jql="project = SANDBOX AND summary ~ \"Test\"", fields=["labels"] |
INSPECTION; one row per issue created above, each with a Labels column |
| 16 | search_jira_issues, jql="nope = 1" |
ERROR with Jira's JQL error message |
4. Check the results in Jira
Open the issues created in steps 2, 4 and 7 and confirm:
- Fields: labels, components, priority, description, and custom fields match the template, and the summary is the one you passed
- Not copied: assignee is empty (or the project default), and reporter is you, not the template's reporter
- Clone link: the new issue shows "clones SANDBOX-1" and the template shows "is cloned by" for each clone. If the direction is reversed, swap
inwardIssueandoutwardIssueinnetra_jira/api.py:create_clone_link - Partial failure (step 7): the issue Jira reported as created is the one whose summary matches the
CREATEDitem, not the failed one
Also try one summary with special characters (for example Test - Variant[A]: Payment+), create it, then run the same call again. The second run must show SKIPPED_EXISTS. If it creates a duplicate, the duplicate search does not handle that character.
5. End-to-end from your AI client
With the server connected to Claude Desktop or Copilot (see Connect your AI client), try the real prompts:
"Clone the template ticket SANDBOX-1 and set the summary to ProjectX Variant-A Payment Service."
"Clone SANDBOX-1 once for each of these services: Payment, Billing, Ledger. Summary format: ProjectX Variant-B Service."
Check that the AI:
- Expands the summary pattern into exact summaries and shows you the dry-run preview before creating anything
- Only applies after you say "apply it"
- Reports each created issue key, and any
SKIPPED_EXISTSorFAILEDitems, rather than a single success message
6. Testing over http (per-user headers)
Start the server in http mode, then connect the Inspector with Transport "Streamable HTTP" and URL http://127.0.0.1:8765/mcp:
SERVER_TRANSPORT=http uv run python server.py
In the Inspector's connection settings, add the headers X-Confluence-User-Email / X-Confluence-Api-Token (or X-Jira-User-Email / X-Jira-Api-Token) and repeat case 1. Then confirm:
- With no headers, every Jira tool returns
{"status": "ERROR", "error": "missing per-user Jira credentials"}, even if credentials are set in the server's.env - With only
X-Jira-User-Emailand noX-Jira-Api-Token, the call fails rather than mixing the Jira email with the Confluence token
7. Clean up
The tools never delete issues. Remove the test issues in Jira yourself, for example by searching project = SANDBOX AND summary ~ "Test" and using Bulk change -> Delete issues.
Jira troubleshooting
| Symptom | Likely cause |
|---|---|
VALIDATION_FAILED with missing required [...] |
The target project requires a field the template does not have. Add it with field_overrides |
VALIDATION_FAILED with issue type '...' not available |
The target project does not have the template's issue type |
ERROR with Permission denied (HTTP 403) |
Your account lacks Create, Edit, or Browse permission on the project |
Issue created but the item has link_error |
Your account lacks Link issues permission, or the Cloners link type is disabled on the site |
ERROR with Rate limited by Jira (HTTP 429) |
Atlassian's rate limit was still in force after 3 attempts. Wait a minute and re-run; existing issues are skipped |
ERROR with Network error on apply |
The response was lost and the request was not retried, because issues may already exist. Re-run the dry run to see which were created |
Development commands
uv sync # install all deps
uv run ruff check . # lint
uv run ruff format --check . # format check
uv run mypy --strict . # type check
uv run python -m pytest --cov=confluence --cov=netra_jira --cov-report=term-missing
README fetched from the repository 5 days ago. The original is authoritative.