redmine-devrelay
v0.10.0
Published
Redmine MCP server for Cursor/Claude/Codex ??read issues and create/comment/update with confirm gate (DevRelay)
Maintainers
Readme
redmine-devrelay
Redmine MCP server for Cursor · Claude Code · Codex.
- Version:
0.10.0 - GitHub: https://github.com/HuijungYoon/devrelay
- Client: redmine-devrelay-client (same version)
Quick start (STDIO)
STDIO is the default transport. Local IDE plugins use this path.
npx -y [email protected]| Env var | Description |
| --- | --- |
| REDMINE_URL | Redmine base URL (may include /redmine path) |
| REDMINE_API_KEY | REST API key |
| REDMINE_ALLOWED_HOSTS | (optional) Host allowlist. Private IPv4 HTTP is allowed separately |
| REDMINE_PREVIEW_STORE_DIR | (optional) Directory for previewToken files. Set it when running several HTTP-mode server processes so a dry-run on one and a confirm on another share tokens. Unset = in-process memory |
| REDMINE_CA_CERT_PATH | (optional) Private CA PEM |
HTTP mode (--http) · BYOK
For remote / Streamable HTTP deployments. The Codex Git marketplace keeps STDIO npx and does not switch to a remote URL.
npx -y [email protected] --http
# or after build
pnpm --filter redmine-devrelay start:http
# port: --port 9090 or PORT (default 8080)Endpoints:
| Path | Description |
| --- | --- |
| POST /mcp | MCP Streamable HTTP |
| GET /healthz | Health check |
| GET /.well-known/openai-apps-challenge | OpenAI Apps challenge (see below) |
BYOK headers (per request)
In production, pass Redmine credentials on each request:
| Header | Description |
| --- | --- |
| X-Redmine-Url | Redmine base URL |
| X-Redmine-Api-Key | REST API key |
Missing headers → 401 (BYOK required).
OPENAI_APPS_CHALLENGE_TOKEN
When set, GET /.well-known/openai-apps-challenge returns the token as plain text. Unset → 404.
HTTP_ALLOW_ENV_FALLBACK=1 (demo only)
When 1, missing BYOK headers fall back to process env REDMINE_URL / REDMINE_API_KEY.
Warning: env credentials are shared by every client of that HTTP process. Demo/local only — turn off in production and use X-Redmine-Url / X-Redmine-Api-Key per request.
Invalid URL / config validation failures → 400 (API key never included in the response). Missing headers → 401.
HTTP session limits
| Env var | Default | Description |
| --- | --- | --- |
| MCP_MAX_SESSIONS | 100 | Concurrent session cap; new initialize returns 503 when full |
| MCP_SESSION_TTL_MS | 1800000 (30m) | Idle TTL; expired sessions pruned on access |
Write rules
dry-run → confirm → confirm=true + previewToken.
You cannot apply without the previewToken from the dry-run response (TTL 10 minutes, single-use).
This Redmine uses HTML bodies. Pass plain text and the client converts it.
| Field | Auto conversion |
| --- | --- |
| description | Plain text lines → <p>…</p> (left as-is if already HTML) |
| notes / comments | Newlines → <br />. Plain text only — Textile/Markdown is blocked in dry-run |
Keeping raw REST out
The confirm gate only protects calls that go through these tools. Two things in the repo close the ways around it:
| Path | What it is |
| --- | --- |
| scripts/redmine-call.mjs | One tool call from a terminal, through the real server — so a session without the MCP tools still gets dry-run -> previewToken -> confirm |
| plugins/claude-code/hooks/ | A PreToolUse hook shipped with the Claude Code plugin. It refuses shell commands and file writes that POST/PUT/DELETE to Redmine directly. Reads are untouched |
previewToken proves a dry-run ran with the same payload — it is not evidence
the user approved. Never call dry-run and confirm=true in the same turn.
Read APIs
| Tool | Description |
| --- | --- |
| redmine_test_connection | Verify connection as the current user with URL and API key |
| redmine_list_projects | List accessible projects |
| redmine_list_project_members | List project members (for assignee / watchers) |
| redmine_search_users | Search all users (may require permission) |
| redmine_search_issues | Search issues (default: open; supports assignedTo: "me"). Rows include dueDate and doneRatio. Filters: trackerId/priorityId/status/fixedVersionId/categoryId by id or name, assignedTo/authorId/watcherId as "me"/id/name, date ranges dueAfter/dueBefore, createdAfter/createdBefore, updatedAfter/updatedBefore (YYYY-MM-DD, inclusive). resolved echoes what each name became |
| redmine_search_text | Full-text search (GET /search.json, Redmine 3.3+): query, optional projectId, types (default issues), titlesOnly, openIssuesOnly. Older servers get an error pointing at subjectContains |
| redmine_get_issue | Issue detail (includes journals, children, etc.) |
| redmine_list_issue_relations | Related issues with their relationId (needed to update/remove one) |
| redmine_list_metadata | Trackers / statuses / priorities / activities (+ versions, categories, custom fields with projectId) as id+name |
| redmine_get_attachment | Download an attachment (attachmentId from redmine_get_issue include=["attachments"]) to destDir (default: OS temp) and return path; text files also return text (first 200 KiB). Only the configured Redmine host is fetched; maxBytes default 10 MiB, hard max 50 MiB |
| redmine_list_time_entries | Time entries by issue / project / user ("me", the default when no issue or project is given) / spentFrom–spentTo; returns totalHours of the returned rows |
Write APIs
| Tool | Description |
| --- | --- |
| redmine_create_issue | Create issue. Dry-run returns wouldApply preview |
| redmine_update_issue | Update issue. Dry-run returns before→after changes[] |
| redmine_add_comment | Add comment (plain text only; Textile/Markdown blocked) |
| redmine_add_attachment | Attach a local file to an existing issue |
| redmine_update_status | Change issue status (statusId) only |
| redmine_bulk_update_status | Same status (id or name) for 1–50 issueIds. Dry-run reads every issue → rows[] (subject, from→to, unchanged/error flags) + one previewToken; confirm applies one by one and returns updated/skipped/failed. Optional plain-text notes on every issue |
| redmine_bulk_update_issue | 1–50 issues in one preview. issues[] holds per-issue fields (doneRatio, statusId, assignedTo, notes …), common applies to every row and a row wins on the same field. Dry-run → rows[] (subject, changes[], unchanged/error) + one previewToken; confirm applies one by one → updated/skipped/failed. Subject, description and parent are single-issue only |
| redmine_log_time | Record a time entry: issueId or projectId, hours, spentOn (default today), activityId (id or name), comments. Dry-run shows the issue subject |
| redmine_add_issue_relation | Link two issues (relationType; delay for precedes/follows) |
| redmine_update_issue_relation | Change a relation by relationId (remove + re-create) |
| redmine_remove_issue_relation | Remove a relation by relationId — both issues stay |
Subtasks (하위일감)
There is no separate subtask tool — the parent link is a field:
| Call | Effect |
| --- | --- |
| redmine_create_issue + parentIssueId | Create a new subtask under that parent |
| redmine_update_issue + parentIssueId: <id> | Attach an existing issue / move it to another parent |
| redmine_update_issue + parentIssueId: null | Detach the subtask — the issue itself is never deleted |
| redmine_search_issues + parentIssueId | List a parent's children |
This server has no issue-delete tool by design; deleting an issue in Redmine is irreversible and cascades to its descendants.
Optional create / update fields
| Field | Meaning |
| --- | --- |
| trackerId | Tracker — id or name (2 or "기능추가") |
| statusId | Status — id or name (2 or "진행") |
| priorityId | Priority — id or name (4 or "긴급") |
| fixedVersionId | Target version — id or name; null clears it (update only) |
| categoryId | Category — id or name; null clears it (update only) |
| customFields | Custom fields [{ id \| name, value }] — name resolves against the project's issue custom fields; "" clears, string array for multi-select; only the listed fields change |
| startDate / dueDate | Start / due date (YYYY-MM-DD) |
| doneRatio | Done ratio (0–100) |
| assignedTo | Assignee ("me" / id / name) |
| watchers | Watchers (id or name array; full replace on update) |
| attachments | Local files [{ path, filename?, description? }] (create / add_attachment) |
| confirm | false (default) = preview (+ previewToken), true = apply (previewToken required) |
| previewToken | Token from dry-run. Invalid if payload changes |
Changelog (summary)
| Version | Notes |
| --- | --- |
| 0.10.0 | redmine_bulk_update_issue — 1–50 issues in one preview, issues[] per-issue and common shared (a row wins on the same field), so a shared status with a different note per issue is one confirm |
| 0.9.2 | Fix: redmine_update_status and redmine_bulk_update_status report the real status name after a write — Redmine answers the PUT with 204, so the status id used to appear in the name ({ id: 2, name: "2" }) |
| 0.9.0 | Time entries (redmine_log_time, redmine_list_time_entries, activities kind), redmine_get_attachment (same-host download, inline text), search filters by name + date ranges and redmine_search_text, redmine_bulk_update_status, journal details, REDMINE_PREVIEW_STORE_DIR file-backed preview tokens |
| 0.8.0 | Custom fields on create/update (customFields: [{ id \| name, value }], names resolved per project, "" clears, arrays for multi-select); customFields kind in redmine_list_metadata with customFieldsSource; Redmine older than 4.2 falls back to /custom_fields.json, then to sampling recent issues |
| 0.7.5 | Fix: assignedTo="me" is resolved to the current user id on create/update — Redmine drops the literal me on writes, so issues were created unassigned |
| 0.7.4 | Ships the Redmine write guard with the Claude Code plugin (PreToolUse hook) and documents scripts/redmine-call.mjs |
| 0.7.3 | Instructions spell out that previewToken is not user approval: dry-run and confirm must not happen in the same turn |
| 0.7.2 | Fix HTML body conversion: plain text with angle brackets is wrapped and escaped again, notes keep every line break, and the tag allowlist matches what Redmine renders. People lookup falls back to recent assignees when the memberships API is forbidden |
| 0.7.1 | dueDate/doneRatio in search results, so a due-date column needs no per-issue fetch |
| 0.7.0 | Names accepted for status/tracker/priority/version/category, redmine_list_metadata, fixedVersionId/categoryId |
| 0.6.0 | Issue relations (list/add/update/remove) and subtasks via parentIssueId; Streamable HTTP + BYOK headers, --http CLI, OpenAI Apps challenge, demo-only env fallback |
| 0.5.2 | English npm README; Claude Code marketplace install (redmine-devrelay plugin id) |
| 0.5.1 | Codex marketplace install CLI alignment (ON_USE, plugin add) + pin |
| 0.5.0 | Block Textile/Markdown in notes, previewToken confirm gate |
| 0.4.1 | Docs/example IP cleanup, Antigravity plugin pin |
| 0.4.0 | Attachments: create attachments + redmine_add_attachment |
| 0.3.3 | Sync npm README, plugin pins, install docs |
| 0.3.2 | Auto-convert plain description → <p> HTML |
| 0.3.1 | notes \n → <br /> |
| 0.3.0 | update_issue, expanded create fields and preview |
| 0.2.x | create / comment / status, members · watchers, private HTTP |
| 0.1.x | Read-only (Phase 1) |
License
MIT
