MCP Redmine
by NeveuGregor
README.md
# MCP Redmine
*🇬🇧 English · [🇫🇷 Français](README.fr.md)*
An MCP (Model Context Protocol) server to drive a Redmine instance from Claude
Desktop or Claude CLI, via Redmine's REST API.
## Requirements
- **Python 3.10+** (the code uses the `X | None` union syntax).
- A Redmine instance with the **REST API enabled**
(*Administration → Settings → API*) and an **API key**.
## Install
```bash
# 1. Clone
git clone https://github.com/NeveuGregor/mcp-redmineApi.git
cd mcp-redmineApi
# 2. Virtual environment
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# 3. Dependencies
pip install -r requirements.txt
# 4. Configuration
cp .env.example .env # then edit .env (URL + API key)
# 5. Smoke-test against your instance
python3 test_connection.py
```
### Redmine API key
*My account → API access key → Show*, then paste it into `.env`
(`REDMINE_API_KEY=...`).
## Configuration
### Environment variables (`.env`)
| Variable | Required | Description |
|---|---|---|
| `REDMINE_URL` | âś… | Base URL (must start with `http://` or `https://`) |
| `REDMINE_API_KEY` | âś… | API access key |
| `REDMINE_DEFAULT_PROJECT_ID` | — | Default project for issue creation |
| `REDMINE_TIMEOUT` | — | HTTP timeout in seconds (default: 30) |
| `DEBUG` | — | Debug logs on **stderr** (default: false) |
| `REDMINE_UPLOAD_DIRS` | — | Allowed upload directories (CSV). **Empty = upload disabled** |
| `REDMINE_EXPOSE_USERS` | — | Expose Redmine identities (default: **false**) |
> ⚠️ **Upload security**: `REDMINE_UPLOAD_DIRS` only allows trusted directories.
> The server can read and upload any file under those paths. Left empty, file
> attachment is refused (safe default).
### Redmine identities (`REDMINE_EXPOSE_USERS`)
The question is not "is this data sensitive" but **who you are sending it to**.
With a sovereign local model, author and assignee are useful for diagnosis. With
a remote model, they must not leave the machine.
| | `false` (default) | `true` |
|---|---|---|
| `author` / `assigned_to` / note writer | stripped **before** parsing | exposed |
| `list_users` tool | not registered (invisible to the model) | available |
| Directory in `get_metadata` | absent, and never even requested from Redmine | included |
| `assigned_to_id` on writes | **works** | works |
Filtering happens in the client layer, not at render time: in restricted mode
this data never transits through the process at all. Writes stay fully
functional — the model can assign to user 42 without being told who 42 is.
> Consequence: the exposed tool list varies by environment (7 or 8). In
> restricted mode an "Assigned to" line **disappears** rather than showing
> "Unassigned" — the information is masked, not absent.
### Claude CLI (Claude Code)
```bash
# Local scope (current project) or user (all your projects)
claude mcp add redmine --scope user python3 -m src.main --cwd /absolute/path/to/redmine-mcp
# Check
claude mcp list
```
### Claude Desktop
In `claude_desktop_config.json`:
```json
{
"mcpServers": {
"redmine": {
"command": "python3",
"args": ["-m", "src.main"],
"cwd": "/absolute/path/to/redmine-mcp"
}
}
}
```
File location: macOS `~/Library/Application Support/Claude/`,
Windows `%APPDATA%\Claude\`, Linux `~/.config/Claude/`.
> Note: the Python package is named `src`; the server runs via
> `python3 -m src.main` from the project root (the `cwd` field).
## Available tools (7, or 8 with `REDMINE_EXPOSE_USERS=true`)
### `create_issue(subject, description, ...)`
Create an issue. Params: `subject`*, `description`*, `project_id`,
`priority_id` (1=Low … 5=Immediate), `tracker_id`, `assigned_to_id`,
`parent_issue_id`, `files` (paths — requires `REDMINE_UPLOAD_DIRS`),
`estimated_hours`.
### `update_issue(issue_id, ...)`
Update an issue. Params: `issue_id`*, `subject`, `description`,
`status_id`, `priority_id`, `assigned_to_id`, `estimated_hours`, `notes`.
### `search_issues(...)`
Search issues. Params: `project_id`, `assigned_to_id`, `status_id`,
`tracker_id`, `subject`, `limit` (default 25), `offset`.
> By default Redmine returns only **open** issues. To widen, pass `status_id`
> as `"open"`, `"closed"`, `"*"` (all), or a specific status id. The result
> reports "X shown out of N total" when truncated.
### `get_issue(issue_id)`
Full detail of an issue: description, metadata (progress, dates, target version),
custom fields, attachments and note history.
### `list_projects()`
List all accessible projects (pagination followed automatically).
### `list_users(name=None, limit=None)`
List or search users (`name` filters server-side on login, first name, last
name, email; `limit` bounds how many are fetched).
> **Registered only if `REDMINE_EXPOSE_USERS=true`.** Otherwise the tool does
> not appear in the MCP tool list.
### `add_time_entry(hours, comments, ...)`
Log time. Params: `hours`*, `comments`*, `issue_id` **or**
`project_id`, `spent_on` (YYYY-MM-DD), `activity_id`.
### `get_metadata()`
Trackers and statuses (ids/names) to feed the other tools. The user directory is
included only if `REDMINE_EXPOSE_USERS=true`.
*\* = required parameter*
## Development & tests
Unit tests mock the HTTP API (`respx`) — no Redmine instance required.
```bash
# Dev dependencies
pip install -r requirements-dev.txt
# Run the suite
python3 -m pytest
# Smoke-test against a real instance (reads .env)
python3 test_connection.py
# Check the config
python3 -c "from src.config import config; print(config.redmine_url)"
```
## Troubleshooting
| Symptom | Likely cause |
|---|---|
| `Access denied (403)` | Invalid API key or insufficient Redmine permissions |
| `Resource not found (404)` | Wrong issue/project id |
| `Invalid data (422)` | Missing required field (Redmine's detail is surfaced) |
| `Network error` | Unreachable URL / timeout — raise `REDMINE_TIMEOUT` |
| `Upload disabled` | Set `REDMINE_UPLOAD_DIRS` |
| `User directory disabled` | Expected: set `REDMINE_EXPOSE_USERS=true` if the target LLM is sovereign |
| No "Assigned to" line in responses | Expected in restricted mode — see `REDMINE_EXPOSE_USERS` |
Debug mode (verbose logs on stderr): `DEBUG=true python3 -m src.main`.
## Architecture
```
src/
main.py entry point (def main)
server.py tool registration + centralized error handling
config.py configuration (.env) validated by pydantic
errors.py RedmineError (status_code, details)
models.py Pydantic models for Redmine entities
redmine_client.py HTTP client (httpx) + parsing
tools/ one module per tool
tests/ pytest suite (respx mock)
```
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues