Skip to main content
Glama

jc-mcp

Token-efficient Jira & Confluence MCP server. Returns lean, flat responses that minimize token consumption while preserving the information LLM agents need.

Quick Start

  1. Get an API token from id.atlassian.com

  2. Add the server to your MCP client:

claude mcp add --scope user jc-mcp \
  -e ATLASSIAN_URL=https://yoursite.atlassian.net \
  -e ATLASSIAN_EMAIL=you@example.com \
  -e ATLASSIAN_TOKEN=your-api-token \
  -- npx -y @maphnet/jc-mcp

The --scope user flag makes the server available across all your projects. Omit it to add only for the current project. CloudId is auto-discovered from your site URL.

Related MCP server: Jira Cloud MCP Server

Updating

npx caches packages per package spec — a cache entry created for @maphnet/jc-mcp (no version) is not refreshed by running npx @maphnet/jc-mcp@latest; that creates a separate entry. To actually update a server configured as npx -y @maphnet/jc-mcp, clear the stale entry:

# find and remove the cached copy used by the un-versioned spec
for d in ~/.npm/_npx/*/node_modules/@maphnet/jc-mcp; do
  echo "$d: $(node -p "require('$d/package.json').version")"
done
rm -rf ~/.npm/_npx/<hash-of-old-version>

Alternatively, configure the server as npx -y @maphnet/jc-mcp@latest so every start resolves the newest release.

If you installed globally (npm i -g @maphnet/jc-mcp, command jc-mcp), update with:

npm i -g @maphnet/jc-mcp@latest

Then restart your MCP client (Claude Code, Cursor, etc.) to pick up the new version.

Tools

Tool

Description

Returns

jcm_getIssue

Read a Jira issue

Flat fields (incl. reporter {displayName, accountId} or null, parent {key, summary, type} or null) + markdown description + attachments + updated/updatedJql; comments carry id, authorAccountId, updated

jcm_getIssues

Batch-read up to 50 issues in one call (one JQL search, plus one GET per key the search omits). fields selects description / comments (default both); description capped at maxDescriptionChars (default 600), comment bodies at maxCommentChars (default 300), 3 most recent comments (default worst case ~75k chars for 50 keys); cut text ends with … [truncated, N chars total]

[{key, summary, type, status, parent, description, comments: [{author, body, created}], commentsTotal?}] in request order, duplicates collapsed; missing or forbidden key → {key, error}; moved/renamed key (resolved by direct GET, relies on Jira resolving old keys) → entry under the requested key plus movedTo (current key)

jcm_getIssueChangelog

Field-change history, oldest first, optionally after since

{entries: [{id, authorAccountId, author, created, items: [{field, from, to}]}], truncated}

jcm_getAttachmentDownloadLink

Resolve a temporary attachment download URL on demand

{attachmentId, filename, url, temporary: true}

jcm_getTransitions

Available workflow transitions

[{id, name}]

jcm_editIssue

Update issue fields and/or apply update ops (e.g. add fix version)

{ok, key}

jcm_transitionIssue

Change workflow state

{ok, key}

jcm_addToActiveSprint

Add an issue to its project's active scrum sprint (no-op if already in one or no scrum board; returns candidates if ambiguous; pass boardId when several scrum boards exist)

{ok, key, sprintId, sprintName, boardId} or {ok, skipped} / {ok: false, reason, candidates}

jcm_createIssue

Create a new issue

{key, url}

jcm_addComment

Add a comment (plain text)

{ok, id}

jcm_searchIssues

JQL search

{total, issues: [{key, summary, status, type, parent, assignee, updated, updatedJql}]}

jcm_getCurrentUser

Get the authenticated user

{accountId, displayName, email, active}

jcm_lookupUser

Find a user by email

[{accountId, displayName, email, active}]

jcm_createVersion

Create a release version

{id, name}

jcm_listVersions

List project versions

[{id, name, released, releaseDate}]

jcm_releaseVersion

Mark a version as released

{ok, id, name}

jcm_addIssueLink

Link two issues: issueKey → targetIssueKey (A blocks B)

{ok, issueKey, targetKey, linkType}

jcm_removeIssueLink

Delete an issue link by ID

{ok, linkId}

jcm_getIssueLinks

List links on an issue

[{id, type, inwardIssue?, outwardIssue?}]

jcm_convertIssueType

Change type of one or more issues, keeping keys (e.g. Tasks → Sub-tasks under a Story); uses Jira bulk move

{ok, keys, issueType, parentKey}

jcm_getArticle

Read a Confluence page

Title + markdown body + spaceId, parentId

jcm_lookupSpace

Resolve a Confluence space key to its numeric ID

{id, key, name}

jcm_createArticle

Create a Confluence page

{id, url}

jcm_updateArticle

Update a Confluence page

{ok, id}

jcm_searchArticles

CQL search

[{id, title, space, spaceKey, lastModified}]

To create a page next to an existing one, pass its spaceId (and parentId) from jcm_getArticle to jcm_createArticle. Otherwise resolve a space key (e.g. spaceKey from jcm_searchArticles) via jcm_lookupSpace. Space names are not unique — don't use them to pick a space.

Jira attachments

Call jcm_getIssue to discover attachments: [{id, filename, mimeType, size, content}] (an empty array when none exist). content is Jira's authenticated content URL; issue reads do not resolve temporary links.

Pass the selected id to jcm_getAttachmentDownloadLink as attachmentId. The server uses its existing Jira credentials and returns a temporary URL plus the filename. Its Jira account needs permission to view the issue and attachment, including applicable issue security restrictions. Download the URL with a local HTTP client without a Jira Authorization header; file bytes stay outside the model context. For example, in a shell, after assigning the returned values to download_url and a local output_path:

curl --fail --location --proto '=https' --proto-redir '=https' --output "$output_path" "$download_url"

Treat the returned URL as a secret: anyone holding it may be able to download the file. Avoid public logs and tickets. URLs expire; call the tool again to obtain a fresh one. No fixed lifetime or permanent public access is promised.

The resolver supports HTTPS redirects to api.media.atlassian.com/file/{id}/binary. Missing or unsupported redirects, direct file responses, and authentication/permission/not-found failures produce errors. It does not host or proxy attachments. A live private Jira Cloud check on 2026-10-16 observed a 303 redirect; downloading its URL without Jira credentials matched the authenticated file byte for byte. One token carried an expiry about ten minutes after the request; that observation is not a lifetime guarantee.

Watching a ticket

Poll with jcm_searchIssues using key = PROJ-1 AND updated > "<updatedJql>", where updatedJql comes from the previous jcm_getIssue or jcm_searchIssues result. It is the issue's updated time floored to the minute in the Jira user's profile timezone, which is the timezone JQL uses. It is null if the timezone lookup failed (retry later). Because of the minute floor, the query re-matches the issue itself and anything changed within that minute. Matching is at-least-once, so compare raw updated values or dedupe by id. The profile timezone is re-read hourly. The no-miss guarantee does not hold during the repeated hour when daylight saving ends: two instants format to the same local minute and JQL may resolve it to the later one. During that hour, also compare raw updated values, or use a lookback of one hour.

On a match, read new comments with jcm_getIssue. Filter them by comment id, created, or updated, and skip your own using authorAccountId from jcm_getCurrentUser. Read field changes with jcm_getIssueChangelog(issueKey, since). Entries are returned oldest first, and since is strict (created > since). When truncated is true, call again with since set to the last returned created.

Why This Server?

Standard Atlassian MCP servers return raw API responses — deeply nested JSON with metadata LLM agents don't need. jc-mcp flattens and trims every response, cutting ~92% of tokens across typical operations:

Operation

jc-mcp

Standard MCP

Reduction

Get issue

~460 tokens

~1,144 tokens

60%

Search (5 issues)

~105 tokens

~6,204 tokens

98%

Get transitions

~17 tokens

~313 tokens

95%

  • Flat fields: status: "In Progress" instead of nested status.name + statusCategory + self links + iconUrl

  • Lean search results: {key, summary, status, type, parent, assignee, updated, updatedJql} per issue (parent is {key, summary, type} or null) — no descriptions, avatars, or project metadata

  • Minimal writes: {ok: true, key: "PROJ-1"} — no re-fetch

Less tokens = faster responses, lower cost, more room in the context window.

Write Confirmation (Elicitation Gate)

Set JCM_CONFIRM_WRITES=true to require user confirmation before every write operation. When enabled, the following tools present a confirmation form (via MCP elicitation) before executing:

  • jcm_createIssue — create a Jira issue

  • jcm_editIssue — update issue fields or apply incremental update ops

  • jcm_transitionIssue — change workflow state

  • jcm_addToActiveSprint — add an issue to the active sprint

  • jcm_addComment — add a comment

  • jcm_createArticle — create a Confluence page

  • jcm_updateArticle — update a Confluence page

The confirmation shows a title and summary of the pending write. The user can:

  • Accept — the write proceeds normally

  • Decline or Cancel — the write is skipped and the tool returns {ok: false, message: "...declined by user."}

  • Timeout (client-dependent) — treated as decline

Without the flag (or with any value other than "true"), all writes proceed immediately — existing behaviour is unchanged.

MCP client requirements

The client must support the elicitation/create capability (MCP spec 2025-06-18+). In Hermes, confirmations appear as approve/deny buttons in Slack. In Claude Code, they appear as inline prompts.

claude mcp add --scope user jc-mcp \
  -e JCM_CONFIRM_WRITES=true \
  -e ATLASSIAN_URL=https://yoursite.atlassian.net \
  -e ATLASSIAN_EMAIL=you@example.com \
  -e ATLASSIAN_TOKEN=your-api-token \
  -- npx -y @maphnet/jc-mcp

Configuration

Variable

Required

Description

ATLASSIAN_URL

Yes

Site URL, e.g. https://yoursite.atlassian.net

ATLASSIAN_EMAIL

Yes

Atlassian account email

ATLASSIAN_TOKEN

Yes

API token

ATLASSIAN_CLOUD_ID

No

Auto-discovered if omitted

JCM_CONFIRM_WRITES

No

Set to "true" to require user confirmation before writes

HTTP transport (shared multi-client server)

By default jc-mcp speaks MCP over stdio, one process per client session. Set JCM_TRANSPORT=http to run one long-lived process per credential set that many clients (Claude Code, Codex, …) share over MCP Streamable HTTP. Every client session gets its own MCP session (isolated responses and write-confirmation prompts). All sessions share one Atlassian client.

Variable

Default

Description

JCM_TRANSPORT

stdio

http enables HTTP mode; anything else keeps stdio

JCM_HTTP_HOST

127.0.0.1

Bind address. Keep it on loopback (see Security)

JCM_HTTP_PORT

3000

Listen port. Use a distinct port per credential set

JCM_HTTP_PATH

/mcp

Endpoint path

JCM_HTTP_IDLE_TIMEOUT_MS

1800000

Sessions with no open stream and no activity for this long are closed. Clients that disconnect cleanly (DELETE) are removed immediately. Sessions with an open stream are force-closed after 4x this timeout

Security: the HTTP endpoint has no authentication. Any local process that can reach the port acts with the configured Atlassian credentials. It is meant for trusted, single-user hosts on loopback only. Requests whose Host header is not 127.0.0.1:<port>, localhost:<port> or [::1]:<port> (or exactly <host>:<port> for a non-loopback bind) get 403 (DNS-rebinding protection); the Origin header, when present, is checked the same way. POST requests must send Content-Type: application/json (415 otherwise), and the request body is capped at 4 MiB (413, checked via Content-Length and enforced during the read). Wildcard binds such as 0.0.0.0 or :: are rejected at startup, including spellings Node normalizes to a wildcard (e.g. 0, 0.0); binding to a specific non-loopback IP is still allowed, but it remains unauthenticated — use at your own risk. Under WSL with mirrored networking, Windows-side processes and rootless Docker containers (via the slirp gateway) can also reach loopback, which falls inside this "local = trusted" model.

systemd user services (one per credential set)

Use one template unit, ~/.config/systemd/user/jc-mcp@.service:

[Unit]
Description=jc-mcp HTTP server (%i)
After=network-online.target

[Service]
Type=simple
EnvironmentFile=%h/.config/mcp/secrets/%i-jc-mcp.env
EnvironmentFile=%h/.config/jc-mcp/%i.env
Environment=JCM_TRANSPORT=http
# systemd does not load nvm: use the absolute output of `command -v node`
ExecStart=/home/USER/.nvm/versions/node/v24.0.0/bin/node /home/USER/dev/jc-mcp/dist/index.js
Restart=on-failure
RestartSec=2

[Install]
WantedBy=default.target

Per-instance port files, ~/.config/jc-mcp/<set>.env:

# ~/.config/jc-mcp/maphnet.env
JCM_HTTP_PORT=3101
# ~/.config/jc-mcp/publius.env
JCM_HTTP_PORT=3102
# ~/.config/jc-mcp/lektra.env
JCM_HTTP_PORT=3103

EnvironmentFile accepts plain KEY=VALUE lines only. If a secrets file uses export KEY=VALUE, remove the export prefixes.

systemctl --user daemon-reload
systemctl --user enable --now jc-mcp@maphnet jc-mcp@publius jc-mcp@lektra
journalctl --user -u jc-mcp@maphnet -f    # expect: jc-mcp server running via http at http://127.0.0.1:3101/mcp

After upgrading jc-mcp (npm run build), run systemctl --user restart 'jc-mcp@*'.

Client configuration

Claude Code (project .mcp.json):

{
  "mcpServers": {
    "maphnet-jc-mcp": { "type": "http", "url": "http://127.0.0.1:3101/mcp" }
  }
}

Or from the CLI: claude mcp add --transport http maphnet-jc-mcp http://127.0.0.1:3101/mcp.

Codex (~/.codex/config.toml or project .codex/config.toml):

[mcp_servers.maphnet-jc-mcp]
url = "http://127.0.0.1:3101/mcp"

Older Codex releases also need experimental_use_rmcp_client = true at the top level of config.toml.

Migrating from stdio group entries

  1. Start the systemd instances above and confirm each one logs its URL.

  2. In each of ~/.config/mcp/groups/maphnet.json, publius.json and lektra.json, replace the jc-mcp stdio entry (command/args/env sourcing) with an HTTP entry: { "type": "http", "url": "http://127.0.0.1:<port>/mcp" } (ports 3101/3102/3103).

  3. Re-apply the groups in each project: mcp-group rm <group> <dir> then mcp-group add <group> <dir>.

  4. Update Codex config.toml entries as shown above, and re-enable the jc-mcp entries that were disabled for memory reasons.

  5. Restart running Claude Code and Codex sessions. MCP server config is read at session start, so existing sessions keep their old stdio processes until restarted.

  6. Verify in a fresh session: the client lists the jc-mcp tools, and a read-only call such as jcm_getIssue on a known key succeeds.

Memory: stdio vs HTTP

Date measured: 2026-10-12, Node v24.19.0. Procedure: start the server with fake credentials, connect N SDK clients and run tools/list on each, then read ps -o rss= -p <pid>.

Mode

Processes

Sessions

Measured RSS

stdio (node dist/index.js)

1 per session

1

78.2 MB

stdio, 6 sessions

6

6

469.2 MB (6 × above)

HTTP, idle

1

0

90.7 MB

HTTP, 6 sessions

1

6

92.9 MB

The previous npx-launched stdio setup used 1.3-1.7 GB combined RSS across sessions, including the npm wrapper processes.

License

MIT

Available Tools

25 tools
jcm_addCommentAdd Comment to Jira IssueA

Add a comment to a Jira issue. Body accepts markdown (headings, tables, code fences, lists). Returns {ok, commentId}.

Inputs: body (required), issueKey (required).

jcm_addToActiveSprintAdd Issue to Active SprintA
Idempotent

Add a Jira issue to its project's active scrum sprint. No-op if the issue is already in an active sprint or the project has no scrum board. If the board or sprint is ambiguous, returns {ok:false, reason, candidates:[{id,name}]}: for reason multiple-scrum-boards retry with boardId; no-active-sprint and multiple-active-sprints need manual handling in Jira. Returns {ok, key, sprintId, sprintName, boardId} or {ok, skipped}.

Inputs: issueKey (required), boardId.

jcm_convertIssueTypeConvert Jira Issue TypeA
DestructiveIdempotent

Convert one or more Jira issues (same project) to another issue type, keeping their keys. To make tasks sub-tasks, provide parentKey. To make sub-tasks standalone, omit parentKey. Uses Jira's bulk move and waits up to 30s. Returns {ok, keys, issueType, parentKey}, or {ok:false, taskId, status, failed?} if the move failed or is still running.

Inputs: issueKeys (required), issueTypeName (required), parentKey.

jcm_createArticleCreate Confluence ArticleB

Create a new Confluence page. Returns {id, title, url}.

Inputs: body (required), title (required), spaceId (required), parentId.

jcm_createIssueCreate Jira IssueA

Create a new Jira issue. Returns {key, url}. For sub-tasks, pass parentKey.

Inputs: summary (required), issueType (required), projectKey (required), parentKey, +1 more.

jcm_createVersionCreate Jira VersionB

Create a new version (release) in a Jira project. Returns {id, name}.

Inputs: name (required), projectKey (required), description, releaseDate.

jcm_editIssueEdit Jira IssueA
DestructiveIdempotent

Update a Jira issue. Pass 'fields' as {fieldName: value} to set values (description accepts markdown), and/or 'update' for incremental ops like adding a fix version: update={fixVersions: [{add: {name: '1.2.0'}}]}. 'update' values are passed verbatim, not markdown-converted. At least one is required. Returns {ok, key}.

Inputs: issueKey (required), fields, update.

jcm_getArticleGet Confluence ArticleA
Read-onlyIdempotent

Fetch a Confluence page by ID. Returns {id, title, body (markdown), status, version, spaceId, parentId?}.

Inputs: pageId (required).

jcm_getCurrentUserGet Current UserA
Read-onlyIdempotent

Returns the authenticated Atlassian user's account info: accountId, displayName, email, active.

Inputs: no named parameters.

jcm_getIssueGet Jira IssueA
Read-onlyIdempotent

Fetch a Jira issue by key. Returns lean fields: key, summary, description (markdown), status, type, parent ({key, summary, type} or null), priority, assignee, reporter ({displayName, accountId} or null), labels, updated (ISO), updatedJql (updated floored to the minute in your Jira timezone, for JQL updated > "..." polling; at-least-once, dedupe; null if unavailable), comments (id, author, authorAccountId, body, created, updated), attachments (id, filename, mimeType, size, content URL). Attachment content URLs require Jira authentication.

Inputs: issueKey (required).

jcm_getIssueChangelogGet Jira Issue ChangelogA
Read-onlyIdempotent

Get a Jira issue's field-change history, oldest first. Returns {entries: [{id, authorAccountId, author, created, items: [{field, from, to}]}], truncated}. since keeps entries created strictly after it. Capped at maxResults oldest matches (entries sharing the last timestamp stay together); if truncated, call again with since = last returned created.

Inputs: issueKey (required), since, maxResults.

jcm_getIssuesGet Jira Issues (batch)A
Read-onlyIdempotent

Batch-read up to 50 Jira issues in one call (one JQL search, plus one GET per key the search omits). Returns an array in request order, one entry per unique key: {key, summary, type, status, parent ({key, summary, type} or null), description (markdown or null), comments: [{author, body, created}], commentsTotal?}. fields selects description and/or comments (default both). Descriptions are capped at maxDescriptionChars (default 600), comment bodies at maxCommentChars (default 300); cut text ends with "… [truncated, N chars total]". Only the 3 most recent comments are returned (oldest first); commentsTotal appears when more exist. A key that is missing or not visible to you returns {key, error} instead of being dropped. A moved/renamed key returns the issue under the requested key plus movedTo (current key). Use jcm_getIssue for full detail on one issue.

Inputs: issueKeys (required), fields, maxCommentChars, maxDescriptionChars.

jcm_getTransitionsGet Issue TransitionsA
Read-onlyIdempotent

Get available workflow transitions for a Jira issue. Returns [{id, name}].

Inputs: issueKey (required).

jcm_listVersionsList Jira VersionsA
Read-onlyIdempotent

List all versions in a Jira project. Returns [{id, name, released, releaseDate}].

Inputs: projectKey (required).

jcm_lookupSpaceLookup Confluence SpaceA
Read-onlyIdempotent

Resolve a Confluence space key to its numeric ID for use with jcm_createArticle. Returns {id, key, name}.

Inputs: spaceKey (required).

jcm_lookupUserLookup UserA
Read-onlyIdempotent

Look up a Jira user by email address. Returns [{accountId, displayName, email, active}].

Inputs: email (required).

jcm_releaseVersionRelease Jira VersionA
DestructiveIdempotent

Mark a Jira version as released. Returns {ok, id, name}.

Inputs: versionId (required), releaseDate.

jcm_searchArticlesSearch Confluence ArticlesA
Read-onlyIdempotent

Search Confluence pages using CQL. Returns [{id, title, space, spaceKey, lastModified}].

Inputs: cql (required), maxResults.

jcm_searchIssuesSearch Jira IssuesA
Read-onlyIdempotent

Search Jira issues using JQL. Returns {total, issues: [{key, summary, status, type, parent, assignee, updated, updatedJql}]}. parent is {key, summary, type} or null. updatedJql is the issue's updated time floored to the minute in your Jira timezone; poll with key = X AND updated > "<updatedJql>" (at-least-once, dedupe). Zero results are validated: invalid JQL values (unknown issue type, missing parent, etc.) return an error instead of an empty list.

Inputs: jql (required), maxResults.

jcm_transitionIssueTransition Jira IssueA
Destructive

Move a Jira issue to a new workflow state. Use jcm_getTransitions first to get valid transition IDs. Returns {ok, key, transitionId}.

Inputs: issueKey (required), transitionId (required).

jcm_updateArticleUpdate Confluence ArticleA
DestructiveIdempotent

Update a Confluence page. Pass current version number (auto-incremented). Returns {ok, id, version}.

Inputs: body (required), title (required), pageId (required), version (required).

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.9/5.0

    Scored across 25 tools

    Disambiguation4/5

    Most tools target a distinct resource and action, and descriptions clarify overlapping areas such as jcm_getIssues, jcm_getIssue, and jcm_searchIssues. Some update-style tools (jcm_editIssue, jcm_transitionIssue, jcm_convertIssueType, jcm_addToActiveSprint) require careful selection, but the set is generally clear.

    Naming Consistency5/5

    All tools consistently use the jcm_ prefix followed by lowerCamelCase verb-first names. The verbs vary by operation (get, search, create, update, add, remove), but the overall convention is predictable throughout.

    Tool Count4/5

    25 tools is on the heavy side, but it covers two broad domains, Jira and Confluence, with several substantial sub-areas. Each tool appears purposeful, though the surface is larger than a typical focused MCP server.

    Completeness4/5

    The server covers core Jira issue, comment, link, version, sprint, transition, user, and Confluence page workflows quite thoroughly. Some gaps remain, such as deleting issues/pages, editing or deleting comments, and broader board/sprint management, but core workflows are largely supported.

    Maintenance

    ActivityActive
    ResponsivenessLimited data

    Related MCP Connectors

    Related MCP Servers