Jira โ Cloud & Data Center
One tool for Jira Cloud and self-hosted Jira Data Center / Server โ issues, epics, links, sprints, worklogs, and attachments from inside C3.
Overview
c3_jira is a single MCP tool that talks to Jira Cloud (REST v3, email + API token, ADF-aware) and Jira Data Center / Server (REST v2, Personal Access Token, plain-text bodies) behind one action-dispatched interface. Claude Code (and you, via the project dashboard) reads and writes issues without a browser context-switch.
Both deployments, one DTO. services/jira_client.py is a facade over two strategy backends (jira_cloud.py / jira_data_center.py) that normalize both APIs' responses into one shape, with an opaque pagination cursor covering Cloud's nextPageToken and Data Center's startAt offset. The deployment is inferred as cloud for *.atlassian.net / *.jira.com URLs; anything else requires --deployment data_center explicitly.
What you get
| Surface | Where | Use it for |
|---|---|---|
c3_jira MCP tool |
Claude Code | Action-dispatched issue, sprint, and worklog operations from inside an AI session |
c3 jira CLI |
Terminal | One-time login, account switching, default-project pinning |
| Jira tab | Project dashboard (per-project) | My Work board, JQL search, an issue drawer, and an activity view |
/api/jira/* |
Project server | REST endpoints backing the dashboard tab |
Quick start
Three commands to connect a Jira site and pin a working project.
Log in
c3 jira login \
--url https://yoursite.atlassian.net
Cloud is inferred from the *.atlassian.net host. Prompts for your email and an API token (getpass masked); the token goes straight into the OS keyring.
Pin a default project
c3 jira set-default --project PROJ
Lets c3_jira calls that accept project= skip it โ my_issues, list_boards, get_create_metadata, create_issue.
Verify
c3 jira status
Lists every configured account and, for the active one, a server-version + identity probe.
Self-hosted Data Center needs the deployment spelled out:
c3 jira login --url https://jira.example.com --deployment data_center
Anything you want reusable across every project (a personal Cloud account) belongs in the global config:
c3 jira login --global --url https://yoursite.atlassian.net
First call from Claude Code
# Connection + account probe, then your open work
c3_jira(action='status')
c3_jira(action='my_issues')
# Raw JQL
c3_jira(action='search', jql='project = PROJ AND status != Done ORDER BY updated DESC')
# Full detail on one issue
c3_jira(action='get_issue', issue='PROJ-123')
Authentication & security
API tokens / PATs
Cloud uses your email plus an API token from id.atlassian.com โ Security โ API tokens (Basic auth over HTTPS). Data Center uses a username plus a Personal Access Token from your profile's Personal Access Tokens page (Bearer auth). Neither surface scopes the token independently of the account's own Jira permissions โ whatever that user can do in the browser, c3_jira can do through the API. Use a dedicated service account with only the project access you intend to grant the agent, especially before enabling delete_issue.
HTTPS-only, enforced. validate_base_url rejects any base_url that isn't https:// unless --insecure is passed โ storing a token against a plaintext endpoint invites interception. --insecure is meant for local development servers only.
Where the token lives
| OS keyring | Token text, under service c3-jira, account {base_url}|{username}. Backed by Windows Credential Manager, macOS Keychain, or Linux Secret Service. |
.c3/config.json |
Non-secret jira section: a named registry of accounts ({base_url, username, deployment, default_project, verify_tls, ca_bundle}) plus which one is the default. Never the token. |
Deployment inference
_jira_deployment_for checks the host: *.atlassian.net or *.jira.com infers cloud automatically. Any other host needs --deployment data_center on the command line โ c3 jira login refuses with an explicit error rather than guessing.
TLS verification
Default: verify_tls = true. --insecure on c3 jira login disables verification (and allows http://); --ca-bundle <path> points at a custom CA bundle for a self-signed enterprise certificate without disabling verification entirely.
Named accounts, multiple sites
Every login is stored under a name (--name work, --name internal), defaulting to the URL's host label when omitted. Pass account=<name> on any c3_jira call to use a non-default account for that one call; switch the default with c3 jira use --name <name>.
Reusable across projects
c3 jira login --global writes the account to ~/.c3/config.json. Resolution mirrors Bitbucket: the project's jira section wins when its default_account resolves against its own accounts registry; otherwise C3 falls back to the home config. The section is taken wholesale from exactly one file โ project fields are never merged over a home-registered account. That's a deliberate security invariant: a repository's committed config must not be able to override the credential-bound base_url, username, deployment, or TLS settings of an account it did not register. Tokens are additionally keyed by (base_url, username) in the keyring, so a rewritten base_url can never retrieve the original server's token.
Action reference
The MCP tool dispatches by action. Twenty-five actions, grouped below by capability. project falls back to the account's default_project when the action takes one and omits it.
Read & discovery
| Action | Args | Description |
|---|---|---|
status | โ | Every configured account, and for the active one a server-version + identity probe. Works without a valid token. |
whoami | โ | Authenticated user โ display name, deployment-native id, email |
search | jql, fields_list?, limit?, cursor? | Raw JQL, unquoted and passed straight through. limit default 25. |
my_issues | project?, status_category?, limit?, cursor? | Builds assignee = currentUser(), optionally narrowed by project and status category |
get_issue | issue | Full detail: type, status, priority, parent, links (with ids), attachments (id/filename/size), comments, description |
list_projects | query?, limit? | Projects visible to the account, optionally filtered. limit default 50. |
list_transitions | issue | Available workflow transitions โ id, name, target status |
get_create_metadata | project?, issue_type | Field configuration for a project + issue type โ see the createmeta caveat |
search_users | query, limit? | Users matching a name/email fragment; returns the deployment-native id assign needs. limit default 10. |
list_link_types | โ | Every issue-link type with its outward/inward phrasing |
list_boards | project?, limit? | Agile boards, optionally scoped to a project. limit default 50. |
list_sprints | board_id, sprint_state?, limit? | Sprints on a board; sprint_state is active, future, or closed |
list_worklogs | issue | Time entries logged on an issue |
Issue writes
| Action | Args | Description |
|---|---|---|
create_issue | project?, issue_type, summary, description?, fields?, parent? | Pre-validated against create metadata โ missing required fields come back as machine-readable JSON instead of a guess. fields is a JSON object of field ids โ values. |
update_issue | issue, summary?, description?, parent?, fields? | Needs at least one of these. parent='none' clears the epic/parent link. |
comment | issue, body, body_format? | Adds a comment. body_format defaults to text. |
transition | issue, transition, body?, fields? | Moves the issue along its workflow. transition is an id or a name (resolved via list_transitions). |
assign | issue, user | user is the deployment-native id โ accountId on Cloud, username on Data Center. Find it via search_users. |
link_issues | issue, link_type, target | Typed link reading <issue> <link_type> <target>. link_type accepts a catalog name or either directional phrasing โ an inward match flips the pair automatically. |
unlink_issues | link_id | Removes one typed link. Ids appear on get_issue's link lines. |
delete_issue | issue, delete_subtasks? | Permanent, not recoverable via the API. Refuses an issue that still has subtasks unless delete_subtasks=true. |
Sprints, worklogs, attachments
| Action | Args | Description |
|---|---|---|
move_to_sprint | issue, sprint_id | issue is one key or a comma-list. Backed by /rest/agile/1.0, identical on Cloud and Data Center. |
move_to_backlog | issue | Comma-list ok. Moves issues out of any sprint. |
add_worklog | issue, time_spent, body? | time_spent is Jira duration syntax โ '2h 30m', '1d'. body is the worklog comment. |
attach_file | issue, file_path | Uploads one local file, multipart. Capped at 20MB locally โ Jira's own server-side cap still applies on top. |
Argument fall-back. my_issues, list_boards, get_create_metadata, and create_issue fall back to the account's default_project from .c3/config.json when the call omits project=. Use c3 jira set-default --project to pin it.
The full parameter reference also lives on the tool card.
CLI commands
Five subcommands cover account lifecycle and project defaults. Run any with --help for full flags.
| Command | Description |
|---|---|
c3 jira login --url <URL> [--deployment cloud|data_center] [--name <N>] [--username <U>] [--token <T>] [--no-set-default] [--no-verify-login] [--insecure] [--ca-bundle <PATH>] [--global] | Interactive token prompt (getpass masked). Writes to keyring, registers the account under --name (derived from the host if omitted), and makes it the default unless --no-set-default. Runs a connection probe unless --no-verify-login. |
c3 jira logout [--name <N>] | Removes an account from keyring + registry. Defaults to the default account. |
c3 jira status | Lists every configured account, marks the default, and probes connectivity. |
c3 jira use --name <N> | Switch the default account (no token re-entry โ it's already in the keyring). |
c3 jira set-default --project <KEY> [--name <N>] | Pin the project key used by c3_jira calls that accept project= when it's omitted. |
Every subcommand also takes a positional project_path (default: current directory) โ the C3 project whose .c3/config.json to write, unless --global redirects login to ~/.c3.
Project dashboard tab
This is the per-project dashboard, not the multi-project Hub. Run c3 serve (or open a project's session server) to see it โ the Jira tab sits in that sidebar, after Bitbucket and before Credentials. The multi-project Hub (c3 hub) has no Jira view of its own.
| View | Shows |
|---|---|
| My Work board | Your assigned issues grouped by status category into three columns โ To Do, In Progress, Done |
| Search | Free-form JQL against /api/jira/search |
| Issue drawer | Opens on click: full detail, transition buttons, a comment box, assignment |
| Activity | Edit-ledger work linked to issue keys โ PROJ-123 detected in branch names and edit summaries. Works even before you log in. |
Endpoints powering the tab live in cli/server.py under /api/jira/*: status, issues/my, board, search, issue/<key> (GET and POST for create), issue/<key>/transition, issue/<key>/comment, issue/<key>/assign, and activity.
Audit trail โ edit ledger integration
State changes on the Jira side aren't in your local working tree, so they're still appended to the C3 edit ledger. The path is virtual:
jira://<issue-or-project-key>
Logged actions โ identifiers only, bodies and arbitrary field contents are never logged:
| Action | Why it's logged |
|---|---|
create_issue / update_issue | Issue opened or its summary/description/parent/fields edited |
comment | Discussion contribution worth recovering on |
transition | Workflow state changed |
assign | Ownership changed |
link_issues / unlink_issues | Relationship to another issue created or removed |
delete_issue | Permanent โ worth remembering happened at all |
move_to_sprint / move_to_backlog | Sprint membership changed |
add_worklog | Time logged against the issue |
attach_file | Evidence attached |
Validation failures never log. _log_mutation skips any response starting with [jira:error] or [jira:missing-fields] โ nothing actually changed in Jira, so nothing is recorded as a mutation. Read-only actions and status are never logged either.
Read it back with c3_edits(action='history', file='jira://PROJ-123'), or via the activity log's jira_action events (also what powers the dashboard's Activity view).
.c3/config.json โ jira section
Default shape (created on first c3 jira login):
{
"jira": {
"default_account": "yoursite",
"accounts": {
"yoursite": {
"base_url": "https://yoursite.atlassian.net",
"username": "alice@example.com",
"deployment": "cloud",
"default_project": "PROJ",
"verify_tls": true,
"ca_bundle": ""
}
}
}
}
default_account | Which entry in accounts a call resolves to when it doesn't pass account=. |
accounts | Named registry โ one entry per (base_url, username) pair you've logged in as. Never the token. |
base_url | Site URL, no trailing slash, always https:// unless the account was created with --insecure. |
deployment | cloud or data_center โ picks the REST v3 vs v2 backend and the auth header shape. |
default_project | Project key used when a call omits project=. Set via c3 jira set-default. |
verify_tls | false when the account was created with --insecure. |
ca_bundle | Path to a custom CA bundle, if one was passed at login. |
Taken wholesale, never field-merged. When C3 falls back from a project's jira section to the home one, it uses the home section entirely โ it never takes base_url from one file and verify_tls from the other. A repository's config can't selectively override a globally registered account's credential-bound fields. See Authentication.
Example workflows
Triage your open work
c3_jira(action='my_issues', status_category='indeterminate')
c3_jira(action='get_issue', issue='PROJ-123')
File a bug โ pre-validated against metadata
# See what the project + issue type actually require
c3_jira(action='get_create_metadata', project='PROJ', issue_type='Bug')
# Create โ missing required fields come back instead of a guess
c3_jira(action='create_issue', project='PROJ', issue_type='Bug',
summary='Login redirect loops on Safari',
description='Repro steps in thread')
Put an issue under an epic, then link two issues
c3_jira(action='update_issue', issue='PROJ-124', parent='PROJ-42')
c3_jira(action='link_issues', issue='PROJ-1', link_type='blocks', target='PROJ-2')
A field gets rejected on create
# [jira:api-error] ... It is not on the appropriate screen
# Create without the field, then set it separately:
c3_jira(action='update_issue', issue='PROJ-124',
fields='{"customfield_10014": "value"}')
Sprint flow: board โ sprint โ move an issue
c3_jira(action='list_boards')
c3_jira(action='list_sprints', board_id=7, sprint_state='active')
c3_jira(action='move_to_sprint', issue='PROJ-1,PROJ-2', sprint_id=42)
Log time and attach evidence
c3_jira(action='add_worklog', issue='PROJ-1', time_spent='2h 30m', body='Pairing on the redirect fix')
c3_jira(action='attach_file', issue='PROJ-1', file_path='logs/repro.log')
Move an issue through its workflow
c3_jira(action='list_transitions', issue='PROJ-123')
c3_jira(action='transition', issue='PROJ-123', transition='In Progress')
c3_jira(action='comment', issue='PROJ-123', body='Deployed to staging')
Troubleshooting
createmeta is not the create screen. get_create_metadata returns Jira's field configuration for a project + issue type, not what the create screen actually accepts. Jira can list a field as required or optional (Epic Link is the classic case) that create_issue then rejects with "cannot be set. It is not on the appropriate screen." When that happens: create the issue without the field, then set it afterward with update_issue โ the edit screen is configured separately from the create screen in Jira itself, and C3 can't see that difference ahead of time. The tool's own error message repeats this hint. Field entries in the metadata response carry both id and name; the fields JSON you pass to create_issue/update_issue takes ids (customfield_โฆ), never display names.
| Symptom | Cause & fix |
|---|---|
[jira:no-account] |
No default account configured (or the named account= doesn't exist). Run c3 jira login --url <URL>. |
[jira:no-token] |
The account is registered but the keyring entry is missing (e.g. .c3/ copied to a different machine). Re-run c3 jira login for that account to put the token back. |
--deployment cloud|data_center is required for self-hosted URLs |
The host isn't *.atlassian.net / *.jira.com, so C3 can't infer it. Pass --deployment data_center explicitly. |
| HTTP 401 | Bad or expired token, or the token belongs to a different site than base_url. Generate a fresh API token / PAT. |
| HTTP 403 on a write action | The Jira account itself lacks the project permission โ c3_jira has no separate scope model, it acts as that user. Grant the permission in Jira, or use an account that has it. |
get_create_metadata 404s on a Data Center 9.0โ11.x server |
Fixed: newer Data Center versions split the monolithic createmeta endpoint into a paginated pair. The client tries the split endpoints first and falls back to the legacy one on 404, so this should no longer surface โ if it does, the server may be older still. |
SSL: CERTIFICATE_VERIFY_FAILED |
Self-signed enterprise CA. Pass --ca-bundle <path> at login (preferred), or --insecure for local dev only. |
The 'keyring' package is required |
pip install keyring. Declared dependency, but an in-place upgrade can leave it missing. |
Where to look
- Activity log: project dashboard โ Jira tab โ Activity, plus
.c3/activity_log.jsonlfor raw events taggedjira_action. - Edit ledger:
c3_edits(action='history', file='jira://<issue-or-project-key>')for every mutation. - Tool calls:
c3_session(action='log')entries record everyc3_jiradispatch with its action name and (sanitized) args.
Need an action this surface doesn't cover? File an issue or pull request โ the integration is intentionally thin around services/jira_client.py and its Cloud / Data Center backends.