A stateless Model Context Protocol server for Redmine. 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.
uv sync
REDMINE_URL=https://redmine.example.com uv run redmine-mcpThe MCP endpoint is now at http://127.0.0.1:8080/mcp.
docker run --rm -p 8080:8080 \
-e REDMINE_URL=https://redmine.example.com \
ghcr.io/kalvadtech/redmine-mcp:latestThe image is multi-stage Alpine, runs as a non-root user, and exposes 8080.
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.
.mcp.json in your project root:
{
"mcpServers": {
"redmine": {
"type": "http",
"url": "http://127.0.0.1:8080/mcp",
"headers": {
"Authorization": "Bearer your-40-char-key"
}
}
}
}opencode.json in your project root (or your opencode config directory):
{
"$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.
| 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:
--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).
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.
59 tools cover every resource family Redmine exposes through its REST API.
List tools return {items, total_count, limit, offset} for easy paging.
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.uploadstokens,custom_fields,watcher_user_ids)update_issue(incl.notes,private_notes)add_issue_note(thin wrapper)delete_issue
list_issue_relations,get_relation,create_issue_relation(relates / blocks / precedes / etc., withdelayfor precedes/follows),delete_relation
update_journal_note- edit an existing note's text or privacy. Useadd_issue_noteto add a new note.
list_projects,get_projectlist_versions,get_version,create_version,update_version,delete_version
list_memberships,get_membership,add_project_member(user or group),update_membership,remove_membership
list_users,get_user(accepts the literal string"current")get_my_account,update_my_accountlist_groups,get_group,create_group,update_group,delete_group,add_user_to_group,remove_user_from_grouplist_roles,get_role(with permissions)
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
list_wiki_pages,get_wiki_page(with version)create_or_update_wiki_page(PUT-upsert)delete_wiki_page
get_attachment,download_attachment(capped at 25 MiB, base64 out)upload_attachment(returns a token to attach viauploadson create_issue / update_issue)list_files,upload_file(one-shot upload + attach to a project's Files area, with optional version_id and description)
list_news(cross-project or per-project)list_queries(saved queries; pass the id asquery_idtolist_issues)search(full-text across issues, news, documents, wiki, etc.)list_issue_statuses,list_trackers,list_issue_categories,list_custom_fieldslist_issue_priorities,list_time_entry_activities,list_document_categories
For a single Redmine instance, run one container per environment:
docker run -d --name redmine-mcp \
--restart unless-stopped \
-p 8080:8080 \
-e REDMINE_URL=https://redmine.example.com \
ghcr.io/kalvadtech/redmine-mcp:latestPlace 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.
uv sync
uv run pytest
uv run ruff check .
uv run mypy srcThe 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.
Pull requests are welcome. Please:
- follow Conventional Commits,
- keep changes terse and well-scoped (one logical change per commit),
- add or update tests for any behaviour change,
- run
pytest,ruff, andmypyclean before opening the PR.
MIT (c) 2026 Kalvad.