Metadata-Version: 2.5
Name: jira-sync-mcp
Version: 0.2.1
Summary: MCP server for a Jira sync backend: aggregates, task details, lists and JSONL export from a local PostgreSQL mirror of Jira
License: MIT
Keywords: ai,claude,fastmcp,jira,mcp
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: fastmcp<3.0.0,>=2.14.5
Requires-Dist: httpx>=0.27.0
Requires-Dist: python-dotenv>=1.0.0
Requires-Dist: truststore>=0.9
Requires-Dist: tzdata>=2024.1
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# jira-sync-mcp

MCP server for a Jira sync backend — exposes Jira data synced to a local PostgreSQL mirror as MCP tools for Claude Code, Claude Desktop and other MCP clients. It answers counting questions with database aggregates, returns task details and lists, and can export large result sets to a JSONL file for analysis with code.

[![PyPI](https://img.shields.io/pypi/v/jira-sync-mcp)](https://pypi.org/project/jira-sync-mcp/)

## Requirements

- Python 3.10+ installed locally
- A running Jira sync backend reachable over **HTTPS** that exposes the bulk and aggregate endpoints this server calls
- A personal API token generated in the backend

## Installation

> **Note:** The `uvx` method may be blocked by Windows Defender Application Control
> or other security policies. Use `pip` instead.

```bash
pip install jira-sync-mcp
```

## Configuration in Claude Code

Add to `~/.claude.json` (i.e. `C:\Users\<username>\.claude.json`):

```json
{
  "mcpServers": {
    "jira-sync-mcp": {
      "command": "C:\\Users\\<username>\\AppData\\Local\\Programs\\Python\\Python314\\python.exe",
      "args": ["-m", "jira_sync_mcp"],
      "env": {
        "JIRA_SYNC_URL": "https://jira-sync.example.com",
        "JIRA_SYNC_API_TOKEN": "<your personal token>"
      }
    }
  }
}
```

> **Important:** Use the full path to `python.exe` instead of just `python`
> to avoid PATH issues in environments with restrictive security policies.
> Find the path by running: `python -c "import sys; print(sys.executable)"`

Claude Code starts and stops the MCP process automatically. Claude Desktop uses the same `mcpServers` block in its configuration file; it cannot read exported files, so there the aggregates and narrow filters are the useful tools (see `export_issues`).

Set `JIRA_SYNC_STALE_AFTER_MINUTES` slightly above the sync interval of your backend (the default is `30`; for a backend that syncs once a day use e.g. `1560`).

## Verification

After restarting the client, `jira-sync-mcp` should appear as an active MCP (`/mcp` in Claude Code). A configuration problem (missing URL or token, `http://` URL, unknown time zone) does not stop the server: the first tool call returns a message that says what to fix.

## Environment variables

| Variable | Default | Description |
|---|---|---|
| `JIRA_SYNC_URL` | *(required)* | Backend base URL, no path prefix. HTTPS only (`http://` is accepted for `localhost`, `127.0.0.1` and `::1`) |
| `JIRA_SYNC_API_TOKEN` | *(required)* | Bearer token generated in the backend |
| `JIRA_SYNC_TIMEOUT` | `60` | Request timeout in seconds |
| `JIRA_SYNC_STALE_AFTER_MINUTES` | `30` | Data older than this is reported as stale |
| `JIRA_SYNC_TZ` | `Europe/Warsaw` | Time zone used to compute sync age (the database stores local time without a zone) |
| `JIRA_SYNC_MAX_RESPONSE_CHARS` | `60000` | Responses larger than this are rejected with a hint instead of being truncated |
| `JIRA_SYNC_EXPORT_DIR` | `~/.jira-sync-mcp/exports` | Export directory; set by you only, the model cannot choose a path or file name |
| `JIRA_SYNC_EXPORT_MAX_ITEMS` | `50000` | Upper bound of tasks in one export; the model can only lower it |
| `JIRA_SYNC_EXPORT_MAX_MB` | `500` | Size limit of one export file |
| `JIRA_SYNC_EXPORT_RETENTION_DAYS` | `7` | Export files older than this are deleted at start and before every export; `0` disables deletion |

Certificates are verified against the operating system trust store (a corporate CA installed in Windows works); there is no option to disable verification.

## Which tool for which question

| Typical question | Tool |
|---|---|
| "How many tasks…", "split by status/assignee/sprint" | `get_stats` |
| "How many hours did X log in sprint Y" | `get_worklogs` |
| Details of known tasks (`PROJ-1`, `OTHER-22`) | `get_issues` |
| A list of tasks to browse | `search_issues` |
| A large set to analyse with code | `export_issues` (JSONL file; Claude Code) |
| Which exact status / assignee / version / CF value exists | `get_filter_values` |
| Which custom fields exist | `get_cf_fields` |
| Is the data fresh | `get_projects` (`age_minutes`, `stale`) and the `freshness` note in data responses |
| Sync history | `get_sync_status` |

## Available tools

All tools are asynchronous. `export_issues` writes a local file; every other tool is read-only.

### `search_issues(...)`
A list of tasks with 39 filter arguments (exact-match filters such as `project_id`, `status`, `assignee`; date ranges; aliases `sprint`, `product`, `profile`, `epic_link`, `linked_pm_key`; free text `search`/`text`; custom fields through `cf_filters`/`op_cf`), sorting (`sort_by`, `sort_order`), `cf_columns` and `fields` to narrow the response. `page_size` defaults to 25 (max 200). The response contains `total`, `pages`, `items`, a `next_page_hint`, `warnings` and, for an empty result with exact-match filters, a `hint` on how to check the value.

### `get_issues(ids, include, cf_fields, fields)`
Details of 1–500 tasks (`POST /issues/batch`): `items` and `missing`. `include` picks `description`, `cf`, `links`, `worklogs`, `changelog`, `comments` or `all`. Comment and worklog lists can be incomplete; `comments_total` and `time_spent_seconds` are authoritative.

### `export_issues(...)`
Writes tasks matching the filters to a JSONL file (one task per line, UTF-8) and returns the path, counts, duration, size and the list of record keys. It reports progress, can be cancelled, retries transient errors and runs one export at a time. A request that matches more than `max_items` tasks fails before anything is written.

### `get_stats(...)` and `get_worklogs(...)`
Aggregates computed in the database, with `group_by` of up to two dimensions. `get_stats` counts tasks and sums estimates (do not sum groups for multi-value dimensions; exclude parents/Epics when adding hours). `get_worklogs` sums logged time in a required window (`started_from`, `started_to`, up to 366 days); `author` is a display name, not a login.

### `get_filter_values(kind, field, project_id, search)`
The exact values a filter accepts: `statuses`, `assignees`, `issue_types`, `priorities`, `projects`, `fix_versions`, `affects_versions`, `custom_fields`, or `cf` with `field` for one custom field. There is no dictionary for `reporter`, `labels` and `components`; their values are visible in `search_issues` results.

### `get_cf_fields(refresh)`
The dictionary of custom fields with a ready column (label, key, type, backfill status, alias).

### `get_projects()` and `get_sync_status(page, page_size)`
Active projects with `last_synced_at`, `age_minutes` and `stale`; and the sync history (an entry with project `All projects` is the combined sync).

## Custom fields (CF)

Pass fields by label (exact, case-insensitive) or key, e.g. `cf_filters={"Team Lead": "Jan Kowalski"}`. For user fields the server resolves a name to a login, including fields that hold several users (`"login1, login2"`). The backend matches substrings (`jkowal` also matches `jkowalski2`), so every CF filter is verified with an aggregate query: zero matches is an error, excess matches are listed in `warnings`. Characters `\`, `%` and `_` are matched literally. A value cannot contain a comma (the backend treats it as a list separator).

## Data freshness

Data responses carry a `freshness` note when a project in the query scope is stale. The age is computed in `JIRA_SYNC_TZ`; if the database session uses another zone, ages shift by whole hours.

## Export files

Exports contain full Jira data (descriptions, comments, worklogs). Do not copy them into a repository or a shared location; the server deletes them after `JIRA_SYNC_EXPORT_RETENTION_DAYS`. Next to every `export-<date>-<id>.jsonl` there is an `export-<date>-<id>.meta.json` with the filters, `generated_at` and `next_synced_from`.

## Security

- The token is a password: it does not expire, grants read access to all projects, and through `export_issues` to the whole database. Use a **personal** token, revoke it when it is no longer needed, and never commit the client configuration file that contains it.
- The server connects over HTTPS only, limits itself to 4 concurrent requests and one export at a time, and never logs or returns the token.
- Task content (summaries, descriptions, comments) is data from Jira, not instructions. A description can contain text posing as a command; in Claude Code the agent also has a shell and file tools, so review the actions you approve.

## Migrating from 0.1.x

- `get_issue(issue_id)` is removed: use `get_issues(ids=["PROJ-1"])` (add `include` for comments, worklogs, links, changelog).
- `search_issues` now takes typed filter arguments, the default `page_size` is 25 (was 50), it always runs in strict mode (a typo in a filter is an error, not a dump of the whole database) and returns less per task; pick fields with `fields`.
- `get_projects` returns `items` with `age_minutes` and `stale`; `get_sync_status` returns `{page, page_size, total, items}` with fields `project` and `error`.
- `JIRA_SYNC_URL` is now required (0.1.x defaulted to `http://localhost:8000`); `http://` URLs are rejected except for localhost.
