Skip to main content
Glama
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)
```