# Redmine MCP Server [![CI](https://github.com/igorolv/redmine-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/igorolv/redmine-mcp-server/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/igorolv/redmine-mcp-server?include_prereleases)](https://github.com/igorolv/redmine-mcp-server/releases/latest) [![License](https://img.shields.io/github/license/igorolv/redmine-mcp-server)](LICENSE) [![Java 25](https://img.shields.io/badge/Java-25%2B-blue?logo=openjdk)](https://adoptium.net/) [![MCP](https://img.shields.io/badge/MCP-server-8A2BE2)](https://modelcontextprotocol.io/) [![Glama score](https://glama.ai/mcp/servers/igorolv/redmine-mcp-server/badges/score.svg)](https://glama.ai/mcp/servers/igorolv/redmine-mcp-server) [![Listed on mcpservers.org](https://mcpservers.org/badge.svg)](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_=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.json`, `snapshot.json` with snapshot metadata, `attachments.json`, and an `extracted//` directory for derived files. Attachments are materialized under `attachments/` with names like `__` 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", "/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: " /users/current.json ``` Build check: ```bash ./gradlew test ./gradlew build ``` Integration tests against a live Redmine: ```bash REDMINE_URL= REDMINE_API_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: " /users/current.json` - **No search results** — verify that `/search.json` is available in Redmine (it may be disabled by the administrator)