Skip to main content
Glama
README.md
# redmine-mcp

[![ci](https://github.com/KalvadTech/redmine-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/KalvadTech/redmine-mcp/actions/workflows/ci.yml)
[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![python](https://img.shields.io/badge/python-3.14%2B-blue.svg)](https://www.python.org/downloads/)
[![container](https://img.shields.io/badge/container-ghcr.io-2496ED.svg)](https://github.com/KalvadTech/redmine-mcp/pkgs/container/redmine-mcp)

A stateless [Model Context Protocol](https://modelcontextprotocol.io) server
for [Redmine](https://www.redmine.org). Drop it in front of any Redmine
instance and let Claude (or any MCP-aware LLM client) read and write issues,
log time, browse the wiki, and search projects on behalf of the user, with
each user's own API key.

- **Zero state**: no database, no sessions, no shared secret. The server
  forwards each request to Redmine using the API key that came in with it.
- **One server, many users**: the only thing the operator configures is the
  upstream `REDMINE_URL`. Each MCP client supplies its own Redmine API key
  as a Bearer token. Permissions are whatever Redmine says they are.
- **Coverage**: issues (CRUD + notes), projects, memberships, users, time
  entries, wiki (PUT-upsert), attachments (upload + download), full-text
  search, statuses, trackers, categories, custom fields, enumerations.

## Quick start

### Local (uv)

```sh
uv sync
REDMINE_URL=https://redmine.example.com uv run redmine-mcp
```

The MCP endpoint is now at `http://127.0.0.1:8080/mcp`.

### Container (GHCR)

```sh
docker run --rm -p 8080:8080 \
  -e REDMINE_URL=https://redmine.example.com \
  ghcr.io/kalvadtech/redmine-mcp:latest
```

The image is multi-stage Alpine, runs as a non-root user, and exposes 8080.

## Wire it into your MCP client

Find your Redmine API key in `My account > API access key`. The key goes in
the standard `Authorization: Bearer` header; both clients below talk to the
same MCP endpoint over Streamable HTTP.

### Claude Code

`.mcp.json` in your project root:

```json
{
  "mcpServers": {
    "redmine": {
      "type": "http",
      "url": "http://127.0.0.1:8080/mcp",
      "headers": {
        "Authorization": "Bearer your-40-char-key"
      }
    }
  }
}
```

### opencode

`opencode.json` in your project root (or your opencode config directory):

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "redmine": {
      "type": "remote",
      "url": "http://127.0.0.1:8080/mcp",
      "enabled": true,
      "headers": {
        "Authorization": "Bearer {env:REDMINE_API_KEY}"
      }
    }
  }
}
```

opencode supports `{env:VAR}` interpolation in `headers`, so the API key
stays out of the config file: `REDMINE_API_KEY=... opencode`.

The legacy `X-Redmine-API-Key` header is still accepted. If both headers
are sent, the Bearer token wins.

## Configuration

| Variable             | Required | Default | Purpose                                                                                                                                                                                                                                                                                                                                                |
| -------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `REDMINE_URL`        | yes      | -       | Base URL of the Redmine instance (http or https).                                                                                                                                                                                                                                                                                                      |
| `MCP_ALLOWED_HOSTS`  | no       | -       | Comma-separated list of `Host` headers this MCP server will accept (DNS-rebinding protection). Required when running behind a reverse proxy with a public hostname (`Invalid Host header` warnings otherwise). Bare entries also match `host:*`. Set to `*` to disable protection when the proxy already enforces hostnames. Empty keeps localhost-only defaults. |

CLI flags on `redmine-mcp`:

```text
--host        bind address (default 127.0.0.1)
--port        port (default 8080)
--log-level   uvicorn log level (default info)
```

The server fails fast on boot if `REDMINE_URL` is missing or not http(s).

## Authentication model

The Redmine URL is **not** user-supplied; it is fixed per deployment via
`REDMINE_URL`. This eliminates SSRF risk: clients cannot point the server at
arbitrary hosts.

The only credential a client sends is its own Redmine API key, normally in
the `Authorization: Bearer <key>` header (the legacy `X-Redmine-API-Key`
header is accepted as a fallback). The server forwards it to Redmine as
`X-Redmine-API-Key` (so it does not land in Redmine access logs as a query
string), uses it to make one request, then discards it. There is no
caching, no shared service account, no impersonation.

Requests without valid credentials get a `401` with a
`WWW-Authenticate: Bearer` header and a JSON-RPC error body, so strict
MCP clients can surface the auth requirement properly.

## Tools

59 tools cover every resource family Redmine exposes through its REST API.
List tools return `{items, total_count, limit, offset}` for easy paging.

### Issues

- `list_issues` (project, status, assignee, tracker, category, version,
  saved query, free-text query, sort, include)
- `get_issue` (include: journals, attachments, relations, children, watchers)
- `create_issue` (incl. `uploads` tokens, `custom_fields`, `watcher_user_ids`)
- `update_issue` (incl. `notes`, `private_notes`)
- `add_issue_note` (thin wrapper)
- `delete_issue`

### Issue relations

- `list_issue_relations`, `get_relation`, `create_issue_relation`
  (relates / blocks / precedes / etc., with `delay` for precedes/follows),
  `delete_relation`

### Journals (issue notes)

- `update_journal_note` - edit an existing note's text or privacy. Use
  `add_issue_note` to add a new note.

### Projects and versions

- `list_projects`, `get_project`
- `list_versions`, `get_version`, `create_version`, `update_version`,
  `delete_version`

### Memberships

- `list_memberships`, `get_membership`, `add_project_member` (user or
  group), `update_membership`, `remove_membership`

### Users, my account, groups, roles

- `list_users`, `get_user` (accepts the literal string `"current"`)
- `get_my_account`, `update_my_account`
- `list_groups`, `get_group`, `create_group`, `update_group`,
  `delete_group`, `add_user_to_group`, `remove_user_from_group`
- `list_roles`, `get_role` (with permissions)

### Time entries

- `list_time_entries` (date ranges, user, project, issue)
- `create_time_entry` (issue or project, hours, activity, spent_on, comments)
- `update_time_entry`, `delete_time_entry`

### Wiki

- `list_wiki_pages`, `get_wiki_page` (with version)
- `create_or_update_wiki_page` (PUT-upsert)
- `delete_wiki_page`

### Attachments and files

- `get_attachment`, `download_attachment` (capped at 25 MiB, base64 out)
- `upload_attachment` (returns a token to attach via `uploads` on
  create_issue / update_issue)
- `list_files`, `upload_file` (one-shot upload + attach to a project's
  Files area, with optional version_id and description)

### News, queries, search and metadata

- `list_news` (cross-project or per-project)
- `list_queries` (saved queries; pass the id as `query_id` to `list_issues`)
- `search` (full-text across issues, news, documents, wiki, etc.)
- `list_issue_statuses`, `list_trackers`, `list_issue_categories`,
  `list_custom_fields`
- `list_issue_priorities`, `list_time_entry_activities`,
  `list_document_categories`

## Deployment

For a single Redmine instance, run one container per environment:

```sh
docker run -d --name redmine-mcp \
  --restart unless-stopped \
  -p 8080:8080 \
  -e REDMINE_URL=https://redmine.example.com \
  ghcr.io/kalvadtech/redmine-mcp:latest
```

Place it behind your usual reverse proxy and TLS termination. The MCP
protocol is HTTP-only on the server side; users get TLS via your proxy.

For multiple Redmine instances, run one container per instance with its own
`REDMINE_URL` and route by hostname / path at the proxy.

## Development

```sh
uv sync
uv run pytest
uv run ruff check .
uv run mypy src
```

The test suite is fully offline and uses `respx` to mock the Redmine HTTP
API. There are no integration tests against a live Redmine; bring-your-own.

## Contributing

Pull requests are welcome. Please:

- follow [Conventional Commits](https://www.conventionalcommits.org/),
- keep changes terse and well-scoped (one logical change per commit),
- add or update tests for any behaviour change,
- run `pytest`, `ruff`, and `mypy` clean before opening the PR.

## License

[MIT](LICENSE) (c) 2026 Kalvad.