redmine-mcp-server
# Redmine MCP Server
[](https://github.com/igorolv/redmine-mcp-server/actions/workflows/ci.yml)
[](https://github.com/igorolv/redmine-mcp-server/releases/latest)
[](LICENSE)
[](https://adoptium.net/)
[](https://modelcontextprotocol.io/)
[](https://glama.ai/mcp/servers/igorolv/redmine-mcp-server)
[](https://mcpservers.org/servers/igorolv/redmine-mcp-server)
A local MCP server for accessing a corporate Redmine instance. It exposes a limited set of write
operations for issues, time entries, and wiki alongside the read tools; set
`REDMINE_MCP_WRITE_ENABLED=false` to run fully read-only. It lets AI agents (Claude Code, Cursor,
VS Code Copilot, etc.) work with issues, projects, members, versions, wiki, attachments, time
entries, and reference data.
## Quick Start
This documentation covers installing and connecting Redmine MCP Server itself. Installing and
configuring the AI clients themselves is out of scope here.
1. Install JDK 25+.
2. Download `redmine-mcp-server.jar` from the [latest release](https://github.com/igorolv/redmine-mcp-server/releases/latest),
or build it yourself: `./gradlew bootJar` (see [Build](#build)). A [Docker image](#docker) is
published as well.
3. Obtain `REDMINE_URL` and `REDMINE_API_KEY` (see [Configuration](#configuration)).
4. Verify that the JAR starts (see [Smoke Test](#smoke-test)).
5. Add the JAR to your client's MCP configuration (see [Connecting to an AI Client](#connecting-to-an-ai-client)).
For Claude Code that is one command:
```bash
claude mcp add --scope user -e REDMINE_URL=https://redmine.example.com -e REDMINE_API_KEY=your_key -- redmine java -jar /path/to/redmine-mcp-server.jar
```
## Architecture
The server supports only the `stdio` transport.
### Stdio
```
┌─────────────┐ stdio ┌──────────────────┐ REST API ┌──────────┐
│ AI agent │ <------------> │ redmine-mcp- │ -------------> │ Redmine │
│ (Claude Code│ stdin/stdout │ server (Java) │ HTTP + API │ (corp.) │
│ Cursor...) │ │ │ Key │ │
└─────────────┘ └──────────────────┘ └──────────┘
```
The AI client launches the server as a child process; communication follows the MCP protocol
over stdin/stdout.
## Tools
The server exports **32 read-only MCP tools** plus **7 write tools** by default. Set
`REDMINE_MCP_WRITE_ENABLED=false` to remove the write tools and run fully read-only.
### User
| Tool | Description |
|---|---|
| `getCurrentUser` | Current user: ID, login, groups, projects. Useful for "my issues" filtering |
### Projects
| Tool | Description |
|---|---|
| `listProjects` | List of all accessible projects |
| `getProject` | Project details: trackers, modules, description |
| `listProjectMembers` | Project members with roles |
| `listVersions` | Project versions (milestones) |
### Issues
| Tool | Description |
|---|---|
| `listIssues` | Issue list with filters: project, status, tracker, assignee, priority, version, saved query, custom fields (`customFieldFilters` in `cf_<id>=value` format), sorting |
| `searchIssues` | Full-text issue search with detailed results |
| `getIssue` | Issue details: description, status, assignee, dates, notes, relations, custom fields, attachments, linked revisions (`changesets`). Parameters: `issueId`, `focus` (`default`, `implementation`, `timeline`, `full`; optional) |
| `getIssueJournal` | A single complete issue journal note/event without response compression. Parameters: `issueId`, `journalId` |
| `getMyIssues` | Issues of the current user. Parameters: `projectId`, `statusId`, `sort`, `limit`, `offset` |
| `getIssueTree` | Parent chain and subtasks. `root` and `ancestors` keep the `Issue` JSON shape but contain compact context; call `getIssue` for full details or relations. Parameters: `issueId`, `depth` (default 2, max 5) |
| `getIssueHistory` | Creation and every journal event, with status intervals. Long text is shortened; `journalId` identifies the full entry for `getIssueJournal`. Follow `nextOffset` with the optional `offset` parameter until it is absent. |
### Issue, Time Entry, and Wiki Writes (optional)
These tools appear in `tools/list` by default; set `REDMINE_MCP_WRITE_ENABLED=false` to remove them:
| Tool | Description |
|---|---|
| `createIssue` | Creates an issue. Supports core Redmine fields and custom fields via `customFieldsJson` |
| `updateIssue` | Partially updates the specified fields of an existing issue |
| `addIssueNote` | Adds a note to the issue journal |
| `attachFileToIssue` | Uploads a file from any readable local path and attaches it to the issue |
| `createTimeEntry` | Creates a time entry for the user from `REDMINE_API_KEY`; accepts exactly one of `issueId`/`projectId`, hours, date, activity, comment, and custom fields |
| `createWikiPage` | Creates a wiki page and rejects the request if the page already exists |
| `updateWikiPage` | Fully replaces the wiki page text, requiring the optimistic-lock version returned by `getWikiPage` |
Redmine itself enforces the permissions, workflow, and required-field rules of the `REDMINE_API_KEY`
user. The MCP server introduces no additional model of "own" issues or notes. To make AI-created
issues recognizable, the description of an issue created by `createIssue` is prefixed with `AI_EDIT:`
by default (`updateIssue` does not add it). Notes, time-entry comments, wiki revision comments, and
uploaded file names are not marked by default; each marker is configured separately with a
`REDMINE_MCP_WRITE_*_PREFIX` variable (see [Configuration](#configuration)), and an empty value
turns it off. Issue descriptions and notes get the prefix on its own line followed by a blank line;
single-line fields (time-entry and wiki revision comments) get the prefix and a space; file names
get the prefix with no separator. Wiki page markup itself is never changed.
Editing and deleting existing notes is not implemented: the targeted Redmine 4.0.4
does not provide a compatible REST API for it. Editing and deleting time entries; deleting,
renaming, and protecting wiki pages; and creating time entries on behalf of another user are
outside the current operation set.
### Search
| Tool | Description |
|---|---|
| `searchAll` | Global Redmine search: issues, wiki, news, documents, commits, etc. Parameters: `searchQuery`, `projectId`, `types`, `limit`, `offset` |
### Attachments and Wiki
| Tool | Description |
|---|---|
| `getAttachment` | Downloads the original attachment file into a local snapshot directory, returns `localPath`/`fileUri`, and immediately adds text context to `parts[]` if the format is supported: txt/log/xml/json/csv, PDF, Word (.docx), Excel (.xlsx), PowerPoint (.pptx), ZIP. A ZIP may yield a separate part per entry. A .docx yields one text part: pandoc markdown when pandoc is available, POI plain text otherwise. Parameters: `issueId`, `attachmentId`, `maxChars`, `partLimit` (see *Processing Limits*) |
| `getWikiPage` | Content of a project wiki page |
| `listWikiPages` | List of all wiki pages of a project |
| `searchWikiPages` | Full-text search across wiki pages. Parameters: `searchQuery`, `projectId`, `limit`, `offset` |
### Time Entries
| Tool | Description |
|---|---|
| `listTimeEntries` | Logged time with filters: project, issue, user, period |
| `getMyTimeEntries` | Logged time of the current user. Parameters: `projectId`, `issueId`, `from`, `to`, `limit`, `offset` |
### Reference Data
| Tool | Description |
|---|---|
| `listQueries` | Saved queries (custom filters) — ID + name. Use the ID with `listIssues(queryId)` to apply the filter, including custom-field filters |
| `listStatuses` | All issue statuses (ID + name) — for filtering in `listIssues` |
| `listTrackers` | All trackers (ID + name) — for filtering in `listIssues` |
| `listPriorities` | All priorities (ID + name) — for filtering in `listIssues` |
| `listIssueCategories` | Issue categories of a project (ID + name) |
| `listTimeEntryActivities` | Activity types for interpreting existing time entries (ID + name) |
### Analytics
| Tool | Description |
|---|---|
| `getProjectSummary` | Aggregated project summary: overall open/closed count; analyzed open issues broken down by status, tracker, priority, assignee; overdue issues; estimated/spent hours. Analyzes up to 500 open issues and returns a truncation flag. Parameters: `projectId`, `versionId` (optional) |
| `getUserWorkload` | Workload analysis: open issues by project and priority, overdue issues, top issues. Analyzes up to 500 open issues and returns a truncation flag. Parameters: `userId` (optional, defaults to current user), `projectId` (optional) |
| `getVersionChangelog` | Version issues grouped by tracker, open/closed statistics. Analyzes up to 500 issues and returns a truncation flag. Parameters: `projectId`, `versionId` |
| `getBlockerChain` | Recursive traversal of the blocking chain (blocks/blocked_by) upward and downward, limited to depth 10 and 30 loaded issues. Parameters: `issueId` |
| `getStaleIssues` | Open issues not updated for N days, oldest first. Parameters: `projectId`, `daysSinceUpdate` (default 30), `limit` |
| `getReleaseRisks` | Release risk assessment: blockers, overdue items, high-priority issues, unassigned issues. Analyzes up to 500 open issues and returns a truncation flag. Parameters: `projectId`, `versionId` |
| `compareVersions` | Compares two versions: unique issues, shared issues, closure percentage. Analyzes up to 500 issues per version and returns a truncation flag. Parameters: `projectId`, `versionId1`, `versionId2` |
With `REDMINE_MCP_WRITE_ENABLED=false` all tools are read-only and no data in Redmine is modified.
### Tool Groups (enable/disable)
Tools are grouped by domain, and each group can be disabled via an environment variable.
All groups are enabled by default — the out-of-the-box tool manifest is unchanged. Disabling
groups shrinks the MCP `tools/list` manifest that the client loads into model context at session
start. This helps small-context (local) models: disable unneeded groups so only the tools the
model actually needs remain.
| Variable | Group (tools) |
|---|---|
| `REDMINE_MCP_TOOLS_ISSUE` | Issues (core): `listIssues`, `searchIssues`, `getIssue`, `getMyIssues`, `getIssueJournal` |
| `REDMINE_MCP_TOOLS_ISSUE_STRUCTURE` | Issue structure/history: `getIssueTree`, `getIssueHistory` |
| `REDMINE_MCP_TOOLS_PROJECT` | Projects: `listProjects`, `getProject`, `listProjectMembers`, `listVersions` |
| `REDMINE_MCP_TOOLS_SEARCH` | Search: `searchAll` |
| `REDMINE_MCP_TOOLS_ATTACHMENT` | Attachments: `getAttachment` |
| `REDMINE_MCP_TOOLS_WIKI` | Wiki: `getWikiPage`, `listWikiPages`, `searchWikiPages` |
| `REDMINE_MCP_TOOLS_TIME_ENTRY` | Time entries: `listTimeEntries`, `getMyTimeEntries` |
| `REDMINE_MCP_TOOLS_REFERENCE_DATA` | Reference data: `listQueries`, `listStatuses`, `listTrackers`, `listPriorities`, `listIssueCategories`, `listTimeEntryActivities` |
| `REDMINE_MCP_TOOLS_USER` | User: `getCurrentUser` |
| `REDMINE_MCP_TOOLS_ISSUE_ANALYTICS` | Issue analytics: `getBlockerChain`, `getStaleIssues` |
| `REDMINE_MCP_TOOLS_RELEASE_ANALYTICS` | Release/project analytics: `getProjectSummary`, `getUserWorkload`, `getVersionChangelog`, `getReleaseRisks`, `compareVersions` |
Each variable accepts `true` (default) or `false`. Example: to keep only issue and project work,
disable the remaining groups — `REDMINE_MCP_TOOLS_RELEASE_ANALYTICS=false`,
`REDMINE_MCP_TOOLS_WIKI=false`, etc. MCP prompts are not affected by these flags; a prompt tells the model
to skip a step whose tool is disabled.
## MCP Prompts
The server also exports **MCP prompts** for typical issue-handling scenarios. Each takes one required
argument, `issueId` — the Redmine issue number (`12345` or `#12345`).
| Prompt | Description |
|---|---|
| `incident-brief` | Quick incident overview: fetches the issue via `getIssue`, downloads all attachments via `getAttachment` with short previews, and produces a concise Markdown report |
| `incident-implementation` | Implementation context: fetches the issue with `focus=implementation`, picks relevant attachments by name, description and date, recovers compressed journal data only when it matters, and produces requirements, revision evidence, and a verification checklist |
| `incident-timeline` | Incident chronology: uses `getIssue(focus=timeline)` for issue and changeset context, then reads every `getIssueHistory` page for journal events and status intervals |
| `issue-remaining-work` | What is still unfinished: combines `getIssue`, `getIssueTree` (including siblings when relevant), `getBlockerChain`, and `getIssueHistory` when needed, and reports open items without trusting the status alone |
Clients that build command templates by requesting a prompt with a placeholder argument (for example
opencode 1.x, which sends `$1`) get the template with the placeholder left in place, so their own
substitution keeps working.
## Tech Stack
- Java 25, Spring Boot 4.2.0-M2, Spring AI MCP 2.1.0-M1 (stdio transport)
- Apache PDFBox 3.0.5 — text extraction from PDFs
- Apache POI 5.4.0 — text extraction from Word, Excel, PowerPoint
- Apache Tika 3.2.0 (core + parsers-standard) — fallback parser and metadata extraction
- Pandoc (optional, external binary) — improved DOCX to text/markdown conversion when available on `PATH`; otherwise the server uses POI
- Gradle 9.3.1 with version catalog (`gradle/libs.versions.toml`)
## Build
Linux/macOS:
```bash
# Point to JDK 25+ if it is not the default JDK:
export JAVA_HOME="$HOME/.jdks/jdk-25.0.2"
./gradlew build
```
Windows PowerShell:
```powershell
# Point to JDK 25+ if it is not the default JDK:
$env:JAVA_HOME="C:\Program Files\Java\jdk-25"
.\gradlew.bat build
```
Result: `build/libs/redmine-mcp-server.jar`
## Configuration
The server needs `REDMINE_URL` and `REDMINE_API_KEY`; the remaining variables are optional:
| Variable | Description |
|---|---|
| `REDMINE_URL` | Base URL of the Redmine instance (e.g. `https://redmine.example.com`) |
| `REDMINE_API_KEY` | Redmine user's API key |
| `REDMINE_MCP_WRITE_ENABLED` | Adds the `createIssue`, `updateIssue`, `addIssueNote`, `attachFileToIssue`, `createTimeEntry`, `createWikiPage`, `updateWikiPage` tools to `tools/list`; set to `false` to remove them and run read-only; defaults to `true` |
| `REDMINE_MCP_WRITE_ISSUE_DESCRIPTION_PREFIX` | Marker prepended to the description of issues created by `createIssue`; defaults to `AI_EDIT:`. Set to an empty value to disable |
| `REDMINE_MCP_WRITE_ISSUE_NOTE_PREFIX` | Marker prepended to notes added by `addIssueNote`; empty (disabled) by default |
| `REDMINE_MCP_WRITE_TIME_ENTRY_COMMENT_PREFIX` | Marker prepended to `createTimeEntry` comments; counts toward Redmine's 1024-character limit; empty (disabled) by default |
| `REDMINE_MCP_WRITE_WIKI_COMMENT_PREFIX` | Marker prepended to wiki revision comments of `createWikiPage` / `updateWikiPage`; empty (disabled) by default |
| `REDMINE_MCP_WRITE_ATTACHMENT_FILENAME_PREFIX` | Prefix added to file names uploaded by `attachFileToIssue` (e.g. `AI_EDIT__`); must not contain `/ \ ? % * : \| " ' < >`; empty (disabled) by default |
| `REDMINE_MCP_DATA_DIR` | Local data directory of the server; defaults to `~/.redmine-mcp-server` |
| `REDMINE_MCP_HTTP_CONNECT_TIMEOUT_SECONDS` | Timeout for opening a connection to Redmine; defaults to `10` seconds |
| `REDMINE_MCP_HTTP_READ_TIMEOUT_SECONDS` | Timeout for Redmine to answer one request (including the request upload); defaults to `30` seconds. Tool calls are processed one at a time, so a stalled request otherwise delays every later call |
| `REDMINE_MCP_HTTP_SLOW_REQUEST_WARN_MILLIS` | Redmine requests slower than this are logged as warnings; defaults to `5000` ms |
| `REDMINE_MCP_ATTACHMENT_PER_PART_CHARS` | Text limit per single `part` (e.g., one file inside a ZIP) for `getAttachment` calls without limits; defaults to `30000` characters. The tool's `partLimit` parameter replaces it, and so does `maxChars` when `partLimit` is omitted. |
| `REDMINE_MCP_ATTACHMENT_PER_ATTACHMENT_CHARS` | Total limit of extracted text per attachment in `getAttachment` calls without `maxChars`; defaults to `40000` characters, which keeps a default response below `REDMINE_MCP_RESPONSE_MAX_CHARS`. |
| `REDMINE_MCP_ATTACHMENT_MAX_REQUEST_CHARS` | Ceiling for the `maxChars` and `partLimit` a caller passes to `getAttachment`; defaults to `200000` characters. Larger requests are reduced to it and the response `limits.note` says so. |
| `REDMINE_MCP_RELATED_MAX_SIBLINGS` | Maximum sibling issues added to `related` when reading an issue; defaults to `20` |
| `REDMINE_MCP_RELATED_MAX_CHILDREN` | Maximum child issues added to `related` when reading an issue; defaults to `20` |
| `REDMINE_MCP_RELATED_MAX_RELATED` | Maximum related issues from relations added to `related` when reading an issue; defaults to `10` |
`getIssue` resolves the `related` context with one lightweight read of the parent and one batched
lookup for siblings, children and relation targets; related issues are not snapshotted. The
`timeline` and `changesets` focus modes omit `related` and skip these requests entirely.
| `REDMINE_MCP_RESPONSE_MAX_CHARS` | Target response size limit before stepwise compression of `getIssue` and `getAttachment`; defaults to `50000` characters |
| `REDMINE_MCP_RESPONSE_JOURNAL_TAIL_KEEP` | How many most-recent journal entries the budget compression of `getIssue` keeps before more aggressive reduction; defaults to `30` |
| `REDMINE_MCP_RESPONSE_ATTACHMENT_TEXT_PART_CHARS` | Text limit per attachment part during response compression of `getAttachment` calls without `maxChars`/`partLimit`; defaults to `10000` characters. Calls with explicit limits are never truncated below them. |
| `REDMINE_MCP_RESPONSE_JOURNAL_NOTE_CHARS` | Text limit per journal note during response compression of `getIssue`; defaults to `5000` characters |
| `REDMINE_MCP_RESPONSE_IMAGE_PARTS_KEEP` | How many image parts the response compression of `getAttachment` keeps; defaults to `5` |
| `REDMINE_MCP_PAGINATION_DEFAULT_LIMIT` | Default page size for list/search tools; defaults to `25` |
| `REDMINE_MCP_PAGINATION_DEFAULT_OFFSET` | Default offset for list/search tools; defaults to `0` |
| `REDMINE_MCP_PAGINATION_MEMBERS_DEFAULT_LIMIT` | Default page size for `listProjectMembers`; defaults to `100` |
| `REDMINE_MCP_TREE_DEFAULT_DEPTH` | Default depth for `getIssueTree`; defaults to `2` |
| `REDMINE_MCP_TREE_MAX_DEPTH` | Maximum depth of `getIssueTree`; defaults to `5` |
| `REDMINE_MCP_TREE_MAX_ISSUES` | Maximum issues loaded by `getIssueTree`; defaults to `50` |
| `REDMINE_MCP_ANALYSIS_MAX_PAGES` | Maximum Redmine pages read by analytics tools; defaults to `5` |
| `REDMINE_MCP_ANALYSIS_PAGE_SIZE` | Redmine page size for analytics tools; defaults to `100` |
| `REDMINE_MCP_ANALYSIS_TOP_ISSUES_LIMIT` | Maximum issues in top lists of analytics responses; defaults to `10` |
| `REDMINE_MCP_ANALYSIS_MAX_BLOCKER_DEPTH` | Maximum traversal depth for `getBlockerChain`; defaults to `10` |
| `REDMINE_MCP_ANALYSIS_MAX_BLOCKER_ISSUES` | Maximum issues loaded by `getBlockerChain`; defaults to `30` |
| `REDMINE_MCP_ANALYSIS_STALE_ISSUES_DEFAULT_DAYS_SINCE_UPDATE` | Default `daysSinceUpdate` for `getStaleIssues`; defaults to `30` |
| `REDMINE_MCP_ANALYSIS_STALE_ISSUES_DEFAULT_LIMIT` | Default result limit for `getStaleIssues`; defaults to `25` |
| `REDMINE_MCP_ANALYSIS_STALE_ISSUES_MAX_LIMIT` | Maximum result limit for `getStaleIssues`; defaults to `100` |
| `REDMINE_MCP_EXTRACTION_PANDOC_ENABLED` | Enables the use of Pandoc for DOCX when the binary is found on `PATH`; defaults to `true` |
| `REDMINE_MCP_EXTRACTION_PANDOC_PROBE_TIMEOUT_SECONDS` | Timeout for probing Pandoc availability at startup; defaults to `2` seconds |
| `REDMINE_MCP_EXTRACTION_PANDOC_CONVERSION_TIMEOUT_SECONDS` | Timeout for a single DOCX conversion via Pandoc; defaults to `30` seconds |
| `REDMINE_MCP_EXTRACTION_LIMITS_MAX_DEPTH` | Maximum recursion depth for processing nested documents and archives; defaults to `1` |
| `REDMINE_MCP_EXTRACTION_LIMITS_MAX_TOTAL_PARTS` | Maximum text/metadata parts per extraction; defaults to `100` |
| `REDMINE_MCP_EXTRACTION_LIMITS_MAX_TOTAL_BYTES` | Total bytes-read limit per extraction; defaults to `52428800` bytes |
| `REDMINE_MCP_EXTRACTION_LIMITS_MAX_ENTRY_BYTES` | Limit for a single entry inside an archive; defaults to `10485760` bytes |
| `REDMINE_MCP_EXTRACTION_ZIP_MAX_ENTRIES_PER_ARCHIVE` | Maximum entries per ZIP archive; defaults to `100` |
| `REDMINE_MCP_EXTRACTION_TIKA_BODY_LIMIT_BYTES` | Body size limit passed to the Tika fallback parser; defaults to `5242880` bytes |
| `REDMINE_MCP_EXTRACTION_TIKA_METADATA_MAX_FIELDS` | Maximum Tika metadata fields in the response; defaults to `40` |
### How to Get `REDMINE_URL`
Open Redmine in your browser and copy the address-bar value **without** the path — scheme and domain only.
Examples:
| In browser address bar | `REDMINE_URL` value |
|---|---|
| `https://redmine.example.com/projects/myproject` | `https://redmine.example.com` |
| `http://192.168.1.50:3000/issues/123` | `http://192.168.1.50:3000` |
| `http://10.0.0.5/redmine/projects` | `http://10.0.0.5/redmine` |
> If Redmine is reachable only by IP address (no domain name), use the IP as-is, including the port if it differs from the standard one (80/443). If Redmine is deployed under a subpath (e.g. `/redmine`), include it in the URL too.
### How to Get `REDMINE_API_KEY`
1. Log in to Redmine with your account
2. Click **"My account"** (top right corner)
3. In the right column find the **"API access key"** block
4. Click **"Show"** — your personal API key will be displayed
5. Copy the key and use it as the `REDMINE_API_KEY` value
> If the "API access key" block is not shown, contact the Redmine administrator — the REST API may be disabled in settings.
## Smoke Test
Before connecting an AI client it is worth verifying that the JAR starts correctly with the same
environment variables that will later go into the client configuration.
Linux/macOS:
```bash
REDMINE_URL=https://redmine.example.com REDMINE_API_KEY=your_key \
java -jar build/libs/redmine-mcp-server.jar
```
Windows PowerShell:
```powershell
$env:REDMINE_URL="https://redmine.example.com"
$env:REDMINE_API_KEY="your_key"
java -jar .\build\libs\redmine-mcp-server.jar
```
The server runs over `stdio` and opens no HTTP port: after a successful start it silently waits
for MCP requests on `stdin/stdout`. A successful start shows as the absence of errors in the log
and no immediate process exit. Press `Ctrl+C` to stop.
### Docker
The image is published to GHCR with every release:
```bash
docker run -i --rm -e REDMINE_URL=https://redmine.example.com -e REDMINE_API_KEY=your_key ghcr.io/igorolv/redmine-mcp-server:latest
```
The same command is what an MCP client should launch (`-i` keeps stdin open for the stdio
transport). Mount a host directory at `/data` to keep logs and issue snapshots between runs.
To build the image locally: `docker build -t redmine-mcp-server .`
### Logs
Logs are written to `${REDMINE_MCP_DATA_DIR:-~/.redmine-mcp-server}/logs/redmine-mcp-server.log`.
The file rotates by date and size: `10MB`, retention `30` days, total cap `512MB`.
### Issue Snapshots
When loading an issue the server persists a snapshot to disk under
`${REDMINE_MCP_DATA_DIR:-~/.redmine-mcp-server}/issues/<issue-id>/`: `issue.json`,
`snapshot.json` with snapshot metadata, `attachments.json`, and an `extracted/<attachment-id>/`
directory for derived files. Attachments are materialized under `attachments/` with names like
`<attachment-id>__<filename>` and can be reused across snapshots when the local file already
exists and its size matches the Redmine metadata.
## Connecting to an AI Client
Add to the client configuration:
```json
{
"command": "java",
"args": ["-jar", "<absolute-path>/redmine-mcp-server.jar"],
"env": {
"REDMINE_URL": "https://redmine.example.com",
"REDMINE_API_KEY": "your_api_key"
}
}
```
Where exactly:
| Client | How to connect |
|---|---|
| Claude Code | `claude mcp add --scope user -e REDMINE_URL=... -e REDMINE_API_KEY=... -- redmine java -jar /path/to/redmine-mcp-server.jar` |
| Qwen Code | `~/.qwen/settings.json` -> `"mcpServers"` -> `"redmine"` |
| VS Code | `.vscode/mcp.json` -> `"servers"` -> `"redmine"` |
| Cursor | `.cursor/mcp.json` -> `"mcpServers"` -> `"redmine"` |
| Claude Desktop | `claude_desktop_config.json` -> `"mcpServers"` -> `"redmine"` |
Restart the client afterwards.
## Operations and Security
This MCP server is designed to run locally alongside the AI client. It opens no HTTP port and
accepts no incoming network connections: the client launches the JAR as a child process and
communicates with it via `stdin/stdout`.
### Access Model
- The server uses the permissions of the Redmine user whose API key is set in `REDMINE_API_KEY`.
- Seven explicitly listed write tools for issues, time entries, and wiki are available by default;
set `REDMINE_MCP_WRITE_ENABLED=false` to run fully read-only. The server neither extends the API
user's rights nor bypasses the Redmine workflow.
- Accessible projects, issues, attachments, and time entries are determined by the user's permissions
in Redmine. If a user cannot see an object in Redmine, the server must not gain access to it either.
- The `REDMINE_MCP_WRITE_*_PREFIX` markers (such as the default `AI_EDIT:` on created issues) are
search labels, not an authorization mechanism: authorship of a change is reliably established by
the Redmine account that owns the API key.
- `attachFileToIssue` deliberately accepts any readable local path without an allow-list of directories.
Since write tools are on by default, run the server only next to a trusted AI client, keeping in mind
that the client will be able to pass the contents of any file readable by the process into Redmine.
- Treat the API key as a secret. Do not commit it to the repository, shell scripts, `.vscode/mcp.json`,
`.cursor/mcp.json`, or other shared files of the project.
For development and verification of write operations use a separate test Redmine and a separate
API key. Because writes are on by default, set `REDMINE_MCP_WRITE_ENABLED=false` when pointing
`integrationTest` or a manual smoke test at a production installation.
### What Data Is Passed to the AI Client
The AI client receives exactly the data it requests through MCP tools:
- issue cards: subject, description, status, priority, assignee, author, dates, relations, subtasks, journals/comments, custom fields;
- information about projects, versions, members, reference data, and time entries;
- wiki pages;
- attachment metadata;
- local paths to the original attachment files and text extracted from PDF, DOCX, XLSX, PPTX, ZIP, and text files via `getAttachment`.
Before connecting an external or cloud AI client, check your company's internal policies: Redmine
data may contain trade secrets, personal data, logs, keys, error dumps, and document contents.
### Processing Limits
There are protective limits in the code so that a single large document or a related network of
issues cannot overload the MCP client:
| Area | Limit |
|---|---|
| Each text part of `getAttachment.parts[]` | up to 30,000 characters by default, beyond that the text is truncated |
| One attachment in `getAttachment` in total | up to 40,000 characters by default |
| Explicit `maxChars` / `partLimit` in `getAttachment` | honored up to 200,000 characters each |
| ZIP depth | 1 level |
| ZIP archives | up to 100 entries |
| ZIP file inside an archive | up to 10 MB |
| ZIP archive in total | up to 50 MB of extracted data |
| `getIssueTree` | depth up to 5, max 50 issues |
| `getIssueHistory` | Each JSON page fits `REDMINE_MCP_RESPONSE_MAX_CHARS` in both characters and UTF-8 bytes; all events remain available through `nextOffset` |
`getAttachment` reports the limits it applied in `limits` (`maxChars`, `partLimit`, and a `note` when
a requested value was reduced to the ceiling); a cut part carries `totalChars` with its full length.
Without explicit limits, a response that still exceeds `REDMINE_MCP_RESPONSE_MAX_CHARS` (for example,
many ZIP entries) is compressed: extra image parts are collapsed, then text parts are cut to
`REDMINE_MCP_RESPONSE_ATTACHMENT_TEXT_PART_CHARS`. With explicit limits, only image parts are
collapsed. The original file remains on disk at `localPath`; for pandoc DOCX extraction, the
markdown part also points to its saved text file. Other document parts can point to a PDF or Office
source file rather than a separate extracted-text file.
`getIssue` supports the `focus` parameter. `default` keeps the usual
response shape and applies compression only when the response budget is exceeded. `implementation`
targets implementation work on the issue: the full issue is still persisted to disk, while the tool
response keeps the description, human notes, attachment metadata, and all changeset revisions;
verbose history and commit message bodies are omitted. `timeline` targets
"who did what and when" questions: it keeps journals and changesets but omits attachments,
custom fields, and related context. `full` is an explicit choice of the full form with protective
budget compression.
`getIssueTree` still loads and snapshots every fetched issue in full, but returns only compact
`Issue` context for `root` and `ancestors`; their description, journals, custom fields, attachments,
and changesets are omitted from this view. Its `subtree` remains the hierarchy view. This changes
the contents of those existing `Issue` fields: clients that previously read full details from the
tree should call `getIssue(id)` instead. `getIssueTree` does not include direct relations.
`getIssueHistory` includes the creation event and each journal event, including journals without a
note or a recognized field change. It shortens long notes and old/new field values when needed,
keeps `journalId` on each update, and explains reductions in `compressionNotes`. For exact wording
or a complete old/new value, call `getIssueJournal(issueId, journalId)`. Start with
`getIssueHistory(issueId)`; when `nextOffset` is present, pass that number as `offset` in the next
call. Continue until `nextOffset` is absent. `totalEvents` includes creation. Status intervals in
`statusDurations` belong to events on that page; combine them across pages. Compare
`sourceUpdatedOn` across pages and restart if it changes. The server uses the existing
`REDMINE_MCP_RESPONSE_MAX_CHARS` setting; no new environment variable is needed.
If one event cannot fit after text shortening, its page keeps the event and `journalId` but
reduces the event to metadata; `compressionNotes` identify any omitted status intervals and
point to `getIssueJournal` for the full entry.
If the note you need was dropped from the `getIssue` response by budget compression or the note was
shortened, call `getIssueJournal(issueId, journalId)`: it re-takes the issue snapshot and
returns the selected journal entry without response compression.
Some regular list tools (`listIssues`, `listProjects`, `listTimeEntries`, `listQueries`) accept `limit`
and `offset` directly. For reliable operation avoid requesting excessively large pages; a practical
range is 25-100 items per call.
### Diagnostics
Environment check:
```bash
java -version
echo "$REDMINE_URL"
test -n "$REDMINE_API_KEY" && echo "REDMINE_API_KEY is set"
```
Redmine REST API access check:
```bash
curl -H "X-Redmine-API-Key: <key>" <url>/users/current.json
```
Build check:
```bash
./gradlew test
./gradlew build
```
Integration tests against a live Redmine:
```bash
REDMINE_URL=<url> REDMINE_API_KEY=<key> ./gradlew integrationTest
```
Integration tests require a reachable Redmine and real test data. Unit tests exclude tests tagged
`integration` by default.
### Known Operational Limitations
- HTTP timeouts and the retry policy are currently not configurable separately. If Redmine is slow
to respond, an MCP call may wait longer than is convenient for the AI client.
- Redmine errors (`401`, `403`, `404`, `5xx`) are currently handled mostly at the Spring `RestClient`
level; the message seen by the AI-client user may be less friendly than a dedicated MCP tool error.
- A note or attachment may be written successfully, but its new `journalId`/`attachmentId` may stay
undefined if Redmine or a concurrent user modifies the issue between confirmation reads. The operation
itself is still considered completed.
- Search depends on Redmine settings. If `/search.json` is disabled by the administrator,
`searchAll`, `searchIssues`, and `searchWikiPages` may not return the expected results.
- Text extraction from PDF works only for PDFs with a text layer. Scanned documents without OCR
are detected as PDFs with no extractable text.
- Images are not re-encoded. `getAttachment` returns the path to the original file; text `parts[]`
for images remain empty.
## Project Structure
```
├── src/main/java/ru/it_spectrum/ai/redmine/mcp/
│ ├── RedmineMcpServerApplication.java — Spring Boot entry point
│ ├── api/ — stable MCP wire format: records returned by tools/services
│ │ ├── Issue.java
│ │ ├── AttachmentContent.java
│ │ ├── Project.java
│ │ ├── IssueMutationResult.java — stable result of issue write operations
│ │ ├── TimeEntryMutationResult.java — stable result of time-entry creation
│ │ ├── WikiMutationResult.java — stable result of wiki-page writes
│ │ └── ... — tool response DTOs and analytics DTOs
│ ├── client/
│ │ ├── RedmineClient.java — read-only wrapper over the Redmine REST API
│ │ ├── RedmineMutationClient.java — optional POST/PUT for issues, time entries, and wiki
│ │ └── model/ — raw Redmine REST API DTOs, never exported directly to MCP
│ │ ├── RedmineIssue.java
│ │ ├── RedmineAttachment.java
│ │ ├── RedmineProject.java
│ │ └── ...
│ ├── config/
│ │ ├── RedmineClientProperties.java — url + apiKey from env
│ │ ├── RedmineMcpProperties.java — all redmine-mcp.* runtime settings
│ │ ├── RedmineConfig.java — RestClient
│ │ ├── McpServerConfig.java — stdio MCP customizer with immediateExecution(true)
│ │ └── JsonConfig.java — ObjectMapper for MCP JSON
│ ├── extraction/
│ │ ├── ExtractionPipeline.java — document-to-text pipeline
│ │ ├── DocumentParser.java — parser interface
│ │ ├── FileTypeDetector.java — file type detection
│ │ ├── PandocAvailability.java — external pandoc probe at startup
│ │ └── parser/
│ │ ├── PlainTextParser.java — txt/log/csv/json/xml
│ │ ├── PdfTextParser.java — PDF via PDFBox
│ │ ├── DocxTextParser.java — DOCX via POI
│ │ ├── DocxPandocParser.java — DOCX via Pandoc when available
│ │ ├── XlsxTextParser.java — XLSX via POI
│ │ ├── PptxTextParser.java — PPTX via POI
│ │ ├── ZipParser.java — ZIP with bounded recursion
│ │ ├── ImagePassthroughParser.java
│ │ ├── TikaTextFallbackParser.java
│ │ ├── TikaMetadataParser.java
│ │ └── BinaryFallbackParser.java
│ ├── service/
│ │ ├── IssueService.java — issue business logic and mapping client.model -> api
│ │ ├── IssueMutationService.java — optional issue writing and AI marking
│ │ ├── TimeEntryMutationService.java — optional time-entry creation
│ │ ├── WikiMutationService.java — safe wiki writing with optimistic locking
│ │ ├── AttachmentService.java — attachment snapshot, download, and extraction
│ │ ├── IssueSnapshotService.java — local issue and attachment snapshots
│ │ ├── AnalysisService.java — analytics, risks, blocker chain
│ │ └── ... — services for projects, wiki, search, reference data, time entries
│ └── tools/
│ ├── AttachmentTools.java — 1 MCP tool for files and attachment context
│ ├── IncidentPrompts.java — 4 MCP prompts for issue investigation
│ ├── IssueAnalyticsTools.java — 2 issue-analytics MCP tools (blocker chain, stale)
│ ├── IssueStructureTools.java — 2 MCP tools: issue tree and change history
│ ├── IssueTools.java — 5 core issue MCP tools
│ ├── IssueWriteTools.java — 4 optional issue-write MCP tools
│ ├── ProjectTools.java — 4 project MCP tools
│ ├── ReferenceDataTools.java — 6 reference-data MCP tools
│ ├── ReleaseAnalyticsTools.java — 5 release/project analytics MCP tools
│ ├── SearchTools.java — 1 global-search MCP tool
│ ├── TimeEntryTools.java — 2 time-entry MCP tools
│ ├── TimeEntryWriteTools.java — 1 optional time-entry creation MCP tool
│ ├── UserTools.java — 1 current-user MCP tool
│ ├── WikiTools.java — 3 wiki MCP tools
│ └── WikiWriteTools.java — 2 optional wiki-write MCP tools
└── src/main/resources/
├── application.yml — MCP server configuration (stdio)
└── logback-spring.xml — logging configuration
```
## Troubleshooting
- **"Gradle requires JVM 17 or later"** — point `JAVA_HOME` at JDK 25+
- **Connection refused / 401** — check `REDMINE_URL` and `REDMINE_API_KEY`. Test: `curl -H "X-Redmine-API-Key: <key>" <url>/users/current.json`
- **No search results** — verify that `/search.json` is available in Redmine (it may be disabled by the administrator)
TDQS
Scored across 39 tools
Each tool targets a distinct resource and action, and descriptions explicitly cross-reference alternatives (e.g., listIssues vs searchIssues vs getMyIssues, getIssue vs getIssueHistory). The surface is large but boundaries are clearly documented, leaving little true ambiguity.
Consistent camelCase verb_noun pattern throughout (listIssues, getIssue, createWikiPage, searchAll). Minor 'My' variants (getMyIssues, getMyTimeEntries) remain predictable and readable.
39 tools is excessive for a single MCP server; many list* metadata tools and specialized read tools could be consolidated or dynamically discovered, increasing selection burden.
Issue tracking and analytics are well covered, but notable gaps exist: no delete operations for issues, wiki pages, or time entries; no update/delete for time entries; no create/update for projects or versions.