Skip to main content
Glama
Avicennasis

redmine-mcp-workflows

by Avicennasis
README.md
# redmine-mcp-workflows

[![CI](https://github.com/Avicennasis/redmine-mcp-workflows/actions/workflows/test.yml/badge.svg)](https://github.com/Avicennasis/redmine-mcp-workflows/actions/workflows/test.yml)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Avicennasis/redmine-mcp-workflows/badge)](https://scorecard.dev/viewer/?uri=github.com/Avicennasis/redmine-mcp-workflows)
[![Release](https://img.shields.io/github/v/release/Avicennasis/redmine-mcp-workflows?display_name=tag)](https://github.com/Avicennasis/redmine-mcp-workflows/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)
[![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)

A schema-aware Model Context Protocol (MCP) server for Redmine.

Most MCP servers for Redmine are thin wrappers around the REST API: an LLM call goes through, Redmine returns `422 Unprocessable Entity`, and the LLM has to guess what went wrong. `redmine-mcp-workflows` is different — it caches each tracker's workflow graph, custom field schemas, and role-based field permissions, then **validates writes before they leave the client**. When something can't succeed, you get a structured error with a clear next step instead of a stack trace.

## What you get

**92 tools, 746 tests, ruff clean.** See [docs/tool-catalog.md](docs/tool-catalog.md) for the full list. The generic passthrough `redmine_request` is only advertised when `REDMINE_MCP_ENABLE_PASSTHROUGH` is on, so a default deployment lists 91.

### Discovery & introspection (4)
- `redmine_describe_tracker(tracker)` — required fields, allowed status transitions per role, custom field schemas. Cache-backed.
- `redmine_describe_project(project)` — enabled trackers, custom fields, members, default tracker.
- `redmine_list_projects(query=None, limit=25, offset=0)` — paginated.
- `redmine_list_trackers()` — all trackers + cache.

### Cache control (1)
- `redmine_invalidate_cache(scope)` — `"all"`, `"tracker:<id>"`, or `"project:<slug>"`.

### Issue lifecycle (5) — every write validates against the cache
- `redmine_create_issue(project, tracker, subject, ..., difficulty="", quiet=False)`
- `redmine_get_issue(id, include=...)`
- `redmine_update_issue(id, ..., difficulty="", held="", clear_held=False, clear_due_date=False, clear_start_date=False, quiet=False, **fields)` — validates status transitions, custom fields, role permissions.
- `redmine_close_issue(id, note="", clear_held=False, quiet=False)` — convenience over `update_issue` with closure-specific error messaging.
- `redmine_search_issues(query, project=None, status=None, ..., brief=False, fields="")` — `brief=True` (or an explicit `fields` list) returns only the named keys per issue instead of whole issues; the default is unchanged.

#### Holds

`held="<reason>"` sets the `Held` custom field (a reason string — a bare `True`
is rejected with `held_reason_required`) and blocks closing; `clear_held=True`
removes it. The close gate judges the value the update will leave behind, so
`redmine_close_issue(id, clear_held=True)` (or `redmine_update_issue` with a
closed status and `clear_held=True`) releases and closes in one call, while a
close that leaves `Held` non-empty is rejected with `issue_held` and a hint
naming `clear_held=True`.

#### Quiet writes

Write tools return the full issue JSON (~3-5KB per call), which is the fastest
way to exhaust a bulk pipeline's context. Pass **`quiet=True`** —
`redmine_create_issue`, `redmine_update_issue`, `redmine_close_issue`,
`redmine_add_comment`, `redmine_bulk_create_issues` — and a *successful* result
comes back as ids + status only:

```json
{"id": 12345, "status": "ok"}
```

Bulk create returns one compact row per item (`subject`/`status`/`id`/`error`/
`duplicate_of`/`hint`) plus the summary, so a bulk caller can still tell which
items failed. **Errors are never reduced** — a failed write returns full
validation detail in quiet mode too, because that is exactly when a caller needs
it. Default is `False` (unchanged behaviour).

#### Difficulty (engagement-mode signal, v0.4)

If your fleet has a global `Difficulty` custom field configured (values
`Unclassified` / `Easy` / `Normal` / `Hard`), the `difficulty` parameter
on `redmine_create_issue` and `redmine_update_issue` is the convenience
path: pass `difficulty="Easy"` instead of constructing
`custom_fields=[{"id": <N>, "value": "Easy"}]` manually. The field id is
resolved via `/custom_fields.json` and cached.

- **Create** default-fills `Unclassified` when `difficulty` is omitted
  and no `Difficulty` entry is supplied in `custom_fields` — so
  auto-callers don't trip the required-field validation.
- **Update** does NOT default-fill (would silently overwrite user-set
  values).
- If both `difficulty=...` and a matching `custom_fields` entry are
  passed, `difficulty` wins.
- On fleets without a Difficulty field, the parameter silently no-ops.

### Comments & attachments (4)
- `redmine_add_comment(id, note, private=False)`
- `redmine_get_journals(id)`
- `redmine_upload_attachment(file_path, issue_id=None, description=None)` — path-restricted to `REDMINE_MCP_ALLOWED_DIRECTORIES`.
- `redmine_download_attachment(attachment_id, save_to, overwrite=False)` — validates downloaded byte count against metadata `filesize` before writing (no partial-file corruption).

### Time tracking (4) — v0.2
- `redmine_create_time_entry(hours, issue_id=0, project_id=0, activity="", ...)` — accepts `H:MM` or decimal hours; activity names resolve through the cached enumeration.
- `redmine_list_time_entries(issue_id=0, project_id=0, user_id=0, spent_on="", from_date="", to_date="", ...)`
- `redmine_update_time_entry(id, hours="", activity="", ...)` — partial; re-validates supplied hours.
- `redmine_delete_time_entry(id)` — permanent.

### Watchers (3) — v0.2
- `redmine_add_watcher(issue_id, user_id)` — idempotent on the API side.
- `redmine_remove_watcher(issue_id, user_id)` — 404 surfaces verbatim so callers can distinguish "not a watcher" from "issue not found".
- `redmine_list_watchers(issue_id)`

### Wiki page CRUD (4) — v0.2
- `redmine_get_wiki_page(project, title, version=0)` — optional historical version.
- `redmine_create_wiki_page(project, title, text, parent_title="", comments="")` — pre-flight GET refuses to overwrite.
- `redmine_update_wiki_page(project, title, text, version=0, ...)` — optional `version` for optimistic concurrency (mismatch → `redmine_api_409`).
- `redmine_delete_wiki_page(project, title)` — permanent.

### Issue relations (4) — v0.2
- `redmine_list_relations(issue_id)`
- `redmine_add_relation(issue_id, target_issue_id, relation_type, delay=0)` — accepts colloquial `related_to`/`blocked_by`/`duplicate_of` aliases.
- `redmine_remove_relation(relation_id)` — DELETEs `/relations/{id}.json` (top-level URL).
- `redmine_set_parent_issue(issue_id, parent_issue_id)` — pass `0` to unparent.

### Bulk operations (2) — v0.2
- `redmine_bulk_update_issues(issue_ids, ...)` — apply same field updates across many issues. ≤100 per batch.
- `redmine_bulk_close(issue_ids, note="")` — close many at once. Returns `{total, succeeded, failed, skipped}`. `stop_on_error=True` halts on first failure.

### Versions / milestones (6) — v0.2
- `redmine_list_versions(project)`
- `redmine_get_version(version_id)`
- `redmine_create_version(project, name, status="open"|"locked"|"closed", sharing="none"|"descendants"|...)` — enums validated client-side.
- `redmine_update_version(version_id, ...)` — partial updates.
- `redmine_delete_version(version_id)` — 422 if issues still reference it.
- `redmine_assign_issue_to_version(issue_id, version_id)` — pass `version_id=0` to clear.

### Generic passthrough (1) — v0.3, opt-in
- `redmine_request(method, path, body="", params="")` — escape hatch to any Redmine REST endpoint. Gated behind `REDMINE_MCP_ENABLE_PASSTHROUGH=true`, and not advertised in the tool list while that is off. Every response carries `validation_skipped: true` plus a `warning` field. NO validation, NO workflow check, NO schema cache. Use when no validated tool covers your use case; prefer the typed tools whenever possible.

### News & forums (2) — v0.5
- `redmine_list_news(project="", limit=25, offset=0)` — paginated news feed. Empty `project` for the global feed; pass an id or slug for the per-project feed at `/projects/{id}/news.json`.
- `redmine_list_messages(board_id, limit=25, offset=0)` — forum messages on a specific board. A 404 typically means the boards module isn't enabled on the parent project.

## Install

```bash
pip install redmine-mcp-workflows
```

Or for development:

```bash
git clone https://github.com/avicennasis/redmine-mcp-workflows
cd redmine-mcp-workflows
python3 -m venv .venv
.venv/bin/pip install -e .[dev]
```

## Register with Claude Code

```bash
claude mcp add-json -s user redmine '{
  "command": "/path/to/.venv/bin/redmine-mcp",
  "env": {
    "REDMINE_URL": "https://redmine.example.com",
    "REDMINE_API_KEY": "your-api-key"
  }
}'
```

Or for Claude Desktop, add to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "redmine": {
      "command": "/path/to/.venv/bin/redmine-mcp",
      "env": {
        "REDMINE_URL": "https://redmine.example.com",
        "REDMINE_API_KEY": "your-api-key"
      }
    }
  }
}
```

## Configuration

| Variable | Default | Purpose |
|---|---|---|
| `REDMINE_URL` | `http://127.0.0.1:8281` | Redmine base URL. The default loopback is convenient for setups behind a reverse proxy that filters `X-Redmine-API-Key`. |
| `REDMINE_MCP_ALLOWED_HOSTS` | _(empty: any)_ | Optional comma-separated hostname allow-list checked before authenticated clients are created. Entries omit schemes and ports. |
| `REDMINE_API_KEY` | _(none)_ | API key. |
| `REDMINE_OAUTH_TOKEN` | _(none)_ | OAuth2 bearer token (Doorkeeper, Redmine 6.1+). When set, sent as `Authorization: Bearer <token>` and **takes precedence over the API key**. |
| `REDMINE_MCP_READ_ONLY` | `false` | Set `true` to disable write tools. |
| `REDMINE_MCP_CACHE_DIR` | platform user-cache dir | Override for the SQLite cache location. |
| `REDMINE_MCP_CACHE_TTL` | `86400` (24h) | Per-entry TTL in seconds. |
| `REDMINE_MCP_MAX_RESPONSE_BYTES` | `0` (unlimited) | Maximum serialized tool-response size in bytes. Oversized responses return a bounded truncation envelope. |
| `REDMINE_HEADERS` | _(empty)_ | Extra HTTP headers, comma-separated (e.g., `Authorization: Bearer ...`). |
| `REDMINE_MCP_ALLOWED_DIRECTORIES` | `/tmp` | Comma-separated paths that `redmine_upload_attachment` may read from. |
| `REDMINE_MCP_HELD_FIELD_ID` | _(none)_ | Numeric id of the `Held` custom field. Pin it when the field is renamed/localized, otherwise it is matched by the exact English name. |
| `REDMINE_MCP_HELD_UNTIL_FIELD_ID` | _(none)_ | Numeric id of the `Held Until` custom field. |
| `REDMINE_MCP_DIFFICULTY_FIELD_ID` | _(none)_ | Numeric id of the `Difficulty` custom field. |
| `REDMINE_MCP_LOG_LEVEL` | `INFO` | stdlib logging level. |

## Text formatting in notes

Redmine renders issue descriptions and journal notes using a text formatter — **Markdown** on most modern instances. This means `**bold**`, `## headings`, `| col | col |` tables, and `- list items` all render correctly in note text.

**Important for MCP callers:** when composing multi-line notes, the `note` parameter must contain actual newline characters in the JSON string value. Do **not** use literal two-character `\n` escape sequences — they get double-escaped and stored as visible `\n` text instead of line breaks. If your notes look like a single paragraph with visible `\n` markers, this is the cause.

Correct (actual newlines in the JSON string):
```json
{"note": "## Summary\n\nDone:\n- Fixed the auth bug\n- Updated tests"}
```

Wrong (literal backslash-n sequences — renders as one line with visible \n):
```
note = "## Summary\\nDone:\\n- Fixed the auth bug\\n- Updated tests"
```

## Prompt-injection boundary tags

Redmine text is attacker-influenceable, so read responses wrap user-authored
fields (`description`, `notes`, `comments`, `summary`, `text`, `content`) in
`<redmine_user_content>` … `</redmine_user_content>` to mark them as data. Writes
strip those exact tags again — whether they wrap the whole value or appear
inside it as a quoted excerpt — so a read → edit → write-back never stores them
in Redmine. Look-alikes (other case, attributes, HTML-escaped) are left alone.

## How workflow validation works

When Claude calls `redmine_update_issue(id=42, status="Closed")`:

1. Server fetches issue 42's current state.
2. Looks up the workflow row `(tracker_id, current_role, current_status, "Closed")` in the schema cache.
3. If the transition isn't allowed, returns:

   ```json
   {
     "error": "workflow_transition_disallowed",
     "tracker": "Bug",
     "from_status": "In Progress",
     "to_status": "Closed",
     "user_role": "Developer",
     "hint": "Workflow requires status to pass through 'Resolved' first.",
     "allowed_next_states": ["Resolved", "Reopened"]
   }
   ```

4. Otherwise sends the PUT.

Same pattern applies to custom fields (rejects unknown fields and regex/format violations) and required fields (rejects creates that miss required-and-empty fields).

## Differences from other Redmine MCP servers

There are several others — most notably [jztan/redmine-mcp-server](https://github.com/jztan/redmine-mcp-server) (55 tools, comprehensive, no validation), [@onozaty/redmine-mcp-server](https://www.npmjs.com/package/@onozaty/redmine-mcp-server) (TypeScript, Zod schemas), [runekaagaard/mcp-redmine](https://github.com/runekaagaard/mcp-redmine) (generic OpenAPI passthrough). All are good projects.

`redmine-mcp-workflows` differs by **validating before sending** — 92 tools with a schema-aware validation layer that prefers helpful errors over raw API passthrough.

## License

MIT — see [LICENSE](LICENSE).

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for dev setup, testing conventions, and PR flow.

TDQS

B3.4/5.0

Scored across 92 tools

Disambiguation3/5

The set contains many convenience wrappers (e.g., my_issues, today_time_entries, close_issue, assign_issue_to_version) that overlap with general tools like search_issues, list_time_entries, and update_issue. Descriptions generally clarify boundaries, but the sheer number of similar retrieval and mutation paths for issues, time entries, and comments creates moderate misselection risk.

Naming Consistency4/5

All tools use snake_case with a redmine_ prefix, and most follow a verb_noun pattern (list_*, get_*, create_*, update_*, delete_*). A handful are noun phrases (my_issues, today_time_entries, time_report, activity_feed, health, metrics), so minor deviations exist.

Tool Count1/5

92 tools far exceeds the typical 3-15 range and even the 25+ threshold for 'too many.' While Redmine is broad, the surface is inflated with thin wrappers and bulk variants, making the set unwieldy for an agent.

Completeness4/5

Coverage is extensive across issues, projects, time, wiki, news, forums, versions, groups, memberships, categories, watchers, relations, journals, and attachments. Some resources lack full lifecycle (users, roles, custom fields, project files, documents), and there is a passthrough escape hatch, so minor gaps remain.

Maintenance

ActivityActive
ResponsivenessUnresponsive