redmine-mcp
by KalvadTech
README.md
# redmine-mcp
[](https://github.com/KalvadTech/redmine-mcp/actions/workflows/ci.yml)
[](LICENSE)
[](https://www.python.org/downloads/)
[](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.
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessNo issues