MCP and API Tokens for Jira, Confluence, Xray, and Datadog: The Analyst's Write Path
Written by Ahmed at Analyst Engineering, a Senior Technical Business Analyst with 10+ years in banking and payments delivery.
Key takeaways
- MCP is for the conversation and the REST API is for the pipeline. Use an MCP server when you are investigating and want the assistant to fetch context; use a token and a script when the same action has to happen identically on every push.
- Every write must be idempotent and keyed. A publish step that creates a second Confluence page or a duplicate Jira issue on the second run is not a pipeline, it is a mess generator.
- Scope the token to the minimum, store it in a secret manager or the CI secret store, and never let a token with write access sit in a config file an assistant can read.
- Xray closes the last gap in traceability: import the JUnit results from CI with the requirement key on each test, and the coverage view in Jira becomes a live query rather than a status report someone types.
- Datadog is the evidence source nobody uses during requirements work. Query the real error rates and latency percentiles before you write a non-functional requirement, so the number in the spec is the number the system actually produces.
Use MCP when you are investigating and a token-based script when the action has to be identical on every run. An assistant connected to Jira, Confluence, Xray, and Datadog over MCP gives you context while you write requirements. A publish script with a scoped token gives you idempotent writes: the same Confluence page, the same Jira issues, the same Xray execution, every time, keyed so a rerun updates rather than duplicates.
The read-only version of this, and why you start there, is in MCP for analysts. This article is the write path, which is where it gets genuinely useful and genuinely dangerous at the same time. It is stage three of the requirements to UAT pipeline: the step that takes the YAML from requirements as code and puts it where the organisation actually reads. If you want the wider AI, MCP, and agents playbook for analysts, it is in AI at Work: MCP, RAG, and Agents.
When do you use MCP and when do you use a token?
They solve different problems and teams waste weeks conflating them.
| MCP server | API token plus script | |
|---|---|---|
| Runs in | Your assistant, during a conversation | CI, on every push |
| Decides what to call | The model | You, in code |
| Right for | Investigation, ad hoc questions, drafting | Publishing, syncing, importing results |
| Auditability | The chat log | The commit and the run log |
| Failure mode | The model calls something you did not expect | The script does the wrong thing consistently |
| Use it when | You do not know in advance what you need | You know exactly what must happen |
The rule I follow: anything that changes state in a tool other people depend on goes through a script, not through the model. The assistant drafts the content. The pipeline writes it. That separation is what makes AI write access defensible to a security team, and it is the same argument made in AI writes Jira and Confluence.
Tokens: creating them, scoping them, and where they live
Four tools, four different auth models. Get these right once.
Jira and Confluence Cloud. Create an API token at id.atlassian.com under Security. Authenticate with HTTP basic auth: your account email as the username, the token as the password. Atlassian’s scoped tokens let you restrict to named scopes such as read:page:confluence and write:issue:jira, and you should use them. The critical fact: a token inherits the permissions of the account that created it. A token made by an administrator can do everything an administrator can, which is why automation runs as a dedicated service account with access to exactly one project and one space.
Xray Cloud. Two steps. Create an API key in Jira under Apps, Xray, API Keys, which gives you a client id and client secret. Exchange them for a bearer token that is valid for 24 hours, then use that token on the import endpoints.
Datadog. Two credentials, and the distinction matters. An API key identifies the organisation and is used for submitting data. An application key identifies a user and is required for reading, which is what an analyst needs. Scope the application key to the minimum set of authorisation scopes, typically logs_read_data, metrics_read, and apm_read.
Where they live, in order of preference: your CI secret store, then a secret manager (1Password CLI, Vault, AWS Secrets Manager), then a .env file that is in .gitignore and never anywhere else. Never in an MCP server config file that also has write scopes, and never pasted into a chat.
# .env.example committed. The real .env is gitignored.
ATLASSIAN_BASE=https://yourcompany.atlassian.net
ATLASSIAN_EMAIL=svc-requirements@yourcompany.com
ATLASSIAN_TOKEN= # scoped token, service account
CONFLUENCE_SPACE=PAY
JIRA_PROJECT=NP
XRAY_CLIENT_ID=
XRAY_CLIENT_SECRET=
DD_API_KEY=
DD_APP_KEY= # scoped: logs_read_data, metrics_read, apm_read
DD_SITE=datadoghq.eu
Configuring the MCP servers for the read side
For the investigation half, point your assistant at the tools. The Atlassian Remote MCP Server is the supported route for Jira and Confluence Cloud, and it uses OAuth rather than a pasted token, which is the safer default for an interactive assistant.
{
"mcpServers": {
"atlassian": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://mcp.atlassian.com/v1/sse"]
},
"datadog": {
"command": "npx",
"args": ["-y", "@winor30/mcp-server-datadog"],
"env": {
"DATADOG_API_KEY": "${DD_API_KEY}",
"DATADOG_APP_KEY": "${DD_APP_KEY}",
"DATADOG_SITE": "datadoghq.eu"
}
}
}
}
Then the questions that pay for the setup in the first week. These are the six I use while writing requirements:
- “Read Confluence page PAY-1183 and list every business rule stated in it that is not in
requirements/business.yaml.” - “List all open Jira issues in NP with label
iso-migrationthat have no linked requirement key in the description.” - “For endpoint
POST /paymentsin Datadog over the last 30 days, give me p50, p95, p99 latency and the error rate broken down by status code.” - “Which Xray tests in test plan NP-402 have never been executed?”
- “Find every Confluence page in space PAY updated in the last 90 days that mentions the daily limit, and quote the sentence.”
- “Show me the Datadog log pattern counts for
payment.rejectedgrouped by reason code for last month.”
The third and sixth questions are the ones that change requirements work. Writing a performance requirement without querying the current percentiles is guessing, and writing a rejection-handling requirement without knowing which reason codes actually occur in production is guessing twice. The wider skill of reading production evidence is in reading production logs.
The write path: idempotent publishing to Confluence
Here is the rule that separates a pipeline from a mess: every write is keyed, so running it twice changes nothing.
For Confluence, the key is the page title within a space plus a stable marker in the body. Look the page up first, and update if it exists.
# scripts/publish.py Confluence half
import os, requests, markdown
from requests.auth import HTTPBasicAuth
BASE = os.environ["ATLASSIAN_BASE"]
AUTH = HTTPBasicAuth(os.environ["ATLASSIAN_EMAIL"], os.environ["ATLASSIAN_TOKEN"])
SPACE = os.environ["CONFLUENCE_SPACE"]
API = f"{BASE}/wiki/api/v2"
BANNER = ("<ac:structured-macro ac:name='info'><ac:rich-text-body><p>"
"Generated from <code>requirements/business.yaml</code>. "
"Edits here are overwritten on the next push. Raise a pull request instead."
"</p></ac:rich-text-body></ac:structured-macro>")
def upsert(title, html, parent_id):
r = requests.get(f"{API}/pages", auth=AUTH,
params={"title": title, "space-id": SPACE, "body-format": "storage"})
r.raise_for_status()
results = r.json().get("results", [])
body = {"representation": "storage", "value": BANNER + html}
if results:
page = results[0]
# Skip the write entirely if nothing changed. Saves noise in the
# page history and stops every push notifying every watcher.
if page["body"]["storage"]["value"] == body["value"]:
print(f"unchanged: {title}")
return page["id"]
payload = {"id": page["id"], "status": "current", "title": title,
"body": body,
"version": {"number": page["version"]["number"] + 1,
"message": "Regenerated from requirements pipeline"}}
requests.put(f"{API}/pages/{page['id']}", auth=AUTH, json=payload).raise_for_status()
print(f"updated: {title}")
return page["id"]
payload = {"spaceId": SPACE, "status": "current", "title": title,
"parentId": parent_id, "body": body}
r = requests.post(f"{API}/pages", auth=AUTH, json=payload)
r.raise_for_status()
print(f"created: {title}")
return r.json()["id"]
Three details carry the weight. The no-op check stops every push from generating a version and notifying forty watchers. The banner macro tells readers the page is generated, which prevents the single most common failure of this pattern: somebody edits the page, their edit is silently overwritten next Tuesday, and trust in the automation dies. The version message leaves a trail in the page history that matches the commit.
Syncing Jira issues without creating duplicates
Same principle, different key. Use a JQL lookup on a custom field or a label carrying the requirement id, never a title match.
# scripts/publish.py Jira half
import os, yaml, requests
from requests.auth import HTTPBasicAuth
BASE = os.environ["ATLASSIAN_BASE"]; AUTH = HTTPBasicAuth(os.environ["ATLASSIAN_EMAIL"], os.environ["ATLASSIAN_TOKEN"])
PROJ = os.environ["JIRA_PROJECT"]
def find_issue(req_id):
jql = f'project = {PROJ} AND labels = "req:{req_id}" ORDER BY created ASC'
r = requests.get(f"{BASE}/rest/api/3/search/jql", auth=AUTH,
params={"jql": jql, "maxResults": 2, "fields": "summary,status"})
r.raise_for_status()
issues = r.json()["issues"]
if len(issues) > 1:
raise SystemExit(f"FAIL: {req_id} matches {len(issues)} issues. Fix the labels by hand.")
return issues[0] if issues else None
for req in yaml.safe_load(open("requirements/business.yaml", encoding="utf-8")):
if req["status"] != "approved":
continue
existing = find_issue(req["id"])
fields = {
"project": {"key": PROJ},
"summary": f'{req["id"]} {req["title"]}',
"issuetype": {"name": "Story"},
"labels": [f'req:{req["id"]}', f'priority:{req["priority"]}'],
}
if existing:
requests.put(f'{BASE}/rest/api/3/issue/{existing["key"]}', auth=AUTH,
json={"fields": {k: v for k, v in fields.items()
if k in ("summary", "labels")}}).raise_for_status()
print(f'updated {existing["key"]} for {req["id"]}')
else:
r = requests.post(f"{BASE}/rest/api/3/issue", auth=AUTH, json={"fields": fields})
r.raise_for_status()
print(f'created {r.json()["key"]} for {req["id"]}')
The deliberate SystemExit on a duplicate label is important. When automation finds an ambiguous state it should stop and tell a human, not guess which of the two issues is the real one. A pipeline that resolves ambiguity silently is how you end up with two hundred issues nobody trusts.
Note also that this script never sets the status or the assignee. Automation owns the summary and the labels; humans own the workflow. Overreach here is what makes delivery teams disable your integration.
Xray: importing results so coverage becomes a query
This is the step that completes the traceability chain from the pipeline article. CI runs the tests, produces JUnit XML, and imports it into Xray with the requirement key attached to each test.
#!/usr/bin/env bash
# scripts/xray-import.sh run after the test job in CI
set -euo pipefail
TOKEN=$(curl -s -X POST "https://xray.cloud.getxray.app/api/v2/authenticate" \
-H "Content-Type: application/json" \
-d "{\"client_id\":\"${XRAY_CLIENT_ID}\",\"client_secret\":\"${XRAY_CLIENT_SECRET}\"}" | tr -d '"')
curl -s -X POST "https://xray.cloud.getxray.app/api/v2/import/execution/junit" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: text/xml" \
--data-binary "@build/junit.xml" \
-G \
--data-urlencode "projectKey=${JIRA_PROJECT}" \
--data-urlencode "testPlanKey=${TEST_PLAN_KEY}" \
--data-urlencode "testExecKey=" \
--data-urlencode "revision=${GITHUB_SHA}" \
| tee build/xray-import.json
echo "Imported. Execution: $(jq -r '.key' build/xray-import.json)"
Two practices make this worth doing rather than merely possible:
- Put the requirement key in the test name or a Gherkin tag, so Xray links the result to the covering Test issue automatically. A result that lands with no link is a result nobody will ever find again.
- Pass the commit SHA as the revision. When someone asks in six months which version of the code produced the passing run that signed off release 4.2, that field is the answer.
Once results flow in on every run, the Xray coverage view in Jira becomes a live query. The traceability matrix stops being a deliverable someone assembles before an audit and becomes a screenshot anyone can take at any time.
Datadog: the evidence source for non-functional requirements
Most non-functional requirements are invented. “Response time under 500ms”, “availability of 99.9 percent”, “supports 1000 transactions per second”. Ask where the numbers came from and the honest answer is usually a previous project’s document.
One query fixes it. Before writing any NFR, pull the real distribution:
# p95 latency for the endpoint you are about to write a requirement for
curl -s -X POST "https://api.${DD_SITE}/api/v2/query/timeseries" \
-H "DD-API-KEY: ${DD_API_KEY}" -H "DD-APPLICATION-KEY: ${DD_APP_KEY}" \
-H "Content-Type: application/json" \
-d '{
"data": {"type": "timeseries_request", "attributes": {
"from": 1756684800000, "to": 1758153600000,
"queries": [{"name": "p95", "data_source": "metrics",
"query": "p95:trace.http.request.duration{service:payments-api,resource_name:POST_/payments}"}],
"formulas": [{"formula": "p95"}]
}}
}' | jq '.data.attributes.values[0] | {min: min, max: max}'
Then write the requirement with a number that has a source:
- id: NFR-003
type: performance
statement: >
POST /payments responds within 400ms at p95 under the current production
load profile.
baseline: >
Datadog, service payments-api, 2026-08-01 to 2026-09-18: p95 ranged
260ms to 385ms, p99 peaked at 1.4s during the month-end batch window.
source: datadog://dashboard/abc-123-xyz
The baseline field is the difference between a requirement a team can accept and one they will quietly ignore. It also exposes the interesting finding in that example: the p99 spike during month-end, which is a requirement nobody had written and which one query surfaced. More on turning that kind of observation into specification in non-functional requirements.
The security checklist before you turn any of this on
Run through this before the first write, not after the first incident.
- Service account, not your account. A token created by a person acts as that person forever, including after they change teams.
- Minimum scopes. Read scopes for the MCP servers your assistant uses interactively. Write scopes only on the CI token.
- One project, one space. Restrict the service account’s permissions rather than trusting the script to stay in its lane.
- No write tokens in an assistant’s reach. If the model can read your MCP config and your config holds a write token, you have given the model write access you did not intend.
- Rotate on a schedule and on every departure. Ninety days is a reasonable default. Atlassian tokens can be set to expire, so set it.
- Every write leaves a trail. Version messages in Confluence, commit SHAs in Xray, and a run log artifact in CI. If you cannot answer “what changed this page”, the automation is not ready.
- Dry run first. Every publish script gets a
--dry-runflag that prints what it would do. Use it on the first run against a real space, every time.
The takeaway
MCP and API tokens are two halves of the same capability. Give your assistant read access over MCP so it can answer questions from Jira, Confluence, Xray, and Datadog while you work. Give CI a scoped, service-account token so publishing happens the same way on every push, keyed so a rerun updates instead of duplicating. Keep the assistant on the drafting side of the line and the pipeline on the writing side, and AI write access stops being a risk conversation and becomes plumbing.
The most immediately useful piece is the Datadog read, because it changes non-functional requirements from invented numbers to measured ones this week. The next stage of the pipeline is reading the artifacts you were handed rather than the ones you wrote, starting with analyzing diagrams with AI. For the full MCP, RAG, and agents playbook, see AI at Work, or browse everything at The Tech BA Toolkit.
Ahmed is a Senior Technical Business Analyst with 10+ years in banking and payments. He builds practical guides and tools for analysts at The Tech BA Toolkit.
Tags: Business Analysis, AI, MCP, Jira, Automation
About the author
Analyst Engineering is written by Ahmed, a Senior Technical Business Analyst with 10+ years of banking and payments delivery experience: ISO 20022 and SWIFT messaging, payments API integration, Kafka event validation, and production support. Every article comes from real delivery work, and each one is reviewed and updated as tools and standards change.
Related articles
- The Requirements to UAT Pipeline: One Repository From Workshop to Sign-Off A working pipeline that carries a requirement from a workshop transcript to a signed UAT result: eight stages, three machine-readable formats, and four CI gates.
- MCP for Analysts: Connecting AI to Jira and Confluence, Read-Only First What the Model Context Protocol is, how to connect an assistant to Jira and Confluence safely, and the six read-only questions that pay for the setup in a week.
- Automating Jira and Confluence with the REST API and a PAT Use a personal access token and the Jira and Confluence REST APIs to generate traceability matrices and publish specs, with working Python scripts.
- Letting AI Write to Jira and Confluence Without Losing Control The write side of an AI connection: what to automate, what never to, the approval pattern, the dedicated account, and how to keep generated tickets owned by a human.
Free account
Practice on the Labs, keep your progress
A free account, no password: an email link signs you in. It saves your steps and self-assessments on the Labs, shows your missions on a dashboard, unlocks the solutions, and, if you tick the box, sends you new missions and articles when they ship.
Your email is used to sign you in. Nothing else, unless you ask. Privacy.