Redmine MCP OAuth Server
Provides tools to interact with Redmine projects, issues, users, time entries, memberships, groups, versions, custom fields, and queries via the Redmine REST API.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Redmine MCP OAuth Servershow me all open issues in the development project"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
title: Redmine MCP emoji: ๐ง colorFrom: blue colorTo: indigo sdk: docker app_port: 7860 pinned: false
Redmine MCP OAuth Server
A FastMCP HTTP server that exposes 53 Redmine REST endpoints as Model-Context-Protocol tools, behind an OAuth 2.1 Authorization Code flow with mandatory PKCE.
The user logs in with their own Redmine URL + API key โ the server
holds no second account. The API key is validated against
/users/current.json, then mapped to a bearer token used by every
subsequent MCP tool call.
Current version: 1.7 (CHANGELOG.md)
Related MCP server: Redmine MCP Server
Contents
Features
OAuth 2.1 authorization-code flow with mandatory PKCE S256.
RFC 7591 Dynamic Client Registration so MCP clients can self-register.
RFC 8252 loopback redirect URIs auto-allowed (works with every desktop MCP client out of the box).
Refresh tokens with rotation (RFC 6749 ยง6) โ single-use, rolling 30-day TTL.
RFC 7009 revocation at
POST /oauth/revoke.CSRF-protected login form with
itsdangerous-signed cookies.SSRF defense on the user-supplied Redmine URL โ DNS-resolved IPs in private / loopback / link-local / multicast / reserved space are refused.
httpx.AsyncClient(follow_redirects=False)closes 302 bypasses.Per-IP rate limiting on
/auth/login(5/min + 20/hour),/oauth/token(10/min), and/oauth/register(5/min).Pluggable token store โ in-memory by default; opt-in Redis backend with Fernet at-rest encryption of API keys.
Structured JSON audit log to stdout, with per-request IDs.
Sanitized upstream errors โ Redmine's raw response bodies (version banners, plugin names, occasional stack traces) never reach the MCP client. Just
Permission denied./Not found./Upstream Redmine error./ parsed 422 validation errors.Confirm gate on every destructive tool (
delete_*requiresconfirm=True).Health & version endpoints (
/healthz,/readyz,/version).Portable Docker image โ runs unchanged on Hugging Face Spaces (port 7860), Render, Fly.io, Cloud Run, and self-hosted Docker.
Quick start (local Docker)
git clone <repo> && cd redmine_mcp_py
cp .env.example .env
# 1. Generate the secret that signs CSRF cookies
python3 -c 'import secrets; print(secrets.token_urlsafe(32))'
# Paste into .env as REDMINE_MCP_SECRET=...
# 2. (Optional) generate a Fernet key if you want Redis persistence later
python3 -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())'
# 3. Run it
docker compose up --buildOpen http://localhost:8000/healthz โ should return {"status": "ok"}.
The MCP endpoint is http://localhost:8000/mcp.
Configuration
All knobs are env-var-driven. Defaults are safe for local dev and production alike, except where noted.
Required
Variable | Notes |
| 32+ random bytes; signs CSRF cookies. Generate with |
Recommended for production
Variable | Default | Notes |
| (empty) | CSV of Redmine hostnames the server is allowed to call. Empty = any public hostname (private/loopback IPs still blocked). The strongest SSRF defense. |
|
| Set |
| (empty) | e.g. |
| (empty) | Required when |
Token lifetimes (seconds)
Variable | Default | Notes |
|
| Access-token TTL. |
|
| Refresh-token TTL. Rolls on each use. |
|
| Authorization-code TTL. |
|
| DCR client record TTL (Redis only). |
Rate limits (per IP)
Variable | Default |
|
|
|
|
|
|
|
|
Other knobs
Variable | Default | Notes |
|
| Per-call timeout for outbound Redmine HTTP. |
| (empty) | CSV of OAuth |
|
| Dev only. Permits |
|
| Listen port. HF Spaces uses 7860; Render/Fly/Cloud Run inject their own. |
Deployment
Docker Compose (local or self-hosted)
docker-compose.yml ships with sensible local-dev defaults. Two
profiles:
# In-memory store (default profile, ephemeral)
docker compose up --build
# Redis-backed store (sessions survive restart)
docker compose --profile redis up --buildThe Redis profile starts a sibling redis:7-alpine with a persistent
volume and a healthcheck. Set the corresponding env vars first:
REDMINE_MCP_REDIS_URL=redis://redis:6379/0
REDMINE_MCP_FERNET_KEY=<paste-Fernet.generate_key()-output>Hugging Face Spaces
The README.md frontmatter (top of this file) is HF metadata; HF reads
sdk: docker and app_port: 7860 and runs the bundled Dockerfile.
Push the repo to a Space with
sdk: docker.In Settings โ Variables and secrets, set:
REDMINE_MCP_SECRET(required โ generate fresh)REDMINE_MCP_TRUST_PROXY=true(HF sits behind a proxy)REDMINE_MCP_ALLOWED_HOSTS=<your-redmine-host>(recommended)
Restart the Space.
โ HF Spaces free tier sleeps after 15 min idle โ cold start wipes
the in-memory token store, forcing users to reconnect. For always-on
behavior either upgrade to a paid tier or enable Redis (point
REDMINE_MCP_REDIS_URL at an external Redis like Upstash, since HF
doesn't provide one).
Render
The repo includes render.yaml for one-click
Blueprint deploys.
Push to GitHub/GitLab.
Render dashboard โ New + โ Blueprint โ connect the repo โ Apply.
In the service's Environment tab, fill in the two
sync: falsevalues:REDMINE_MCP_ALLOWED_HOSTS(recommended โ CSV of permitted Redmine hostnames)REDMINE_MCP_ALLOWED_REDIRECTS(leave empty for desktop MCP clients)
First build takes ~3 min. You get
https://redmine-mcp-XXXX.onrender.com.
The blueprint defaults to plan: free, which sleeps after 15 min
idle like HF. Change to plan: starter ($7/mo) for always-on, or
keep free and add Render's Key Value (Redis-compatible) addon for
persistence across cold starts.
Fly.io, Cloud Run, Railway, plain VPS
The Dockerfile listens on ${PORT:-7860} and runs as UID 1000 with a
baked-in HEALTHCHECK. It works unchanged on any platform that builds
a Dockerfile.
Minimum env vars: REDMINE_MCP_SECRET, REDMINE_MCP_TRUST_PROXY=true
(when behind a proxy), and ideally REDMINE_MCP_ALLOWED_HOSTS.
For self-hosted behind nginx / Caddy / Traefik, terminate TLS at the
proxy and forward to the container's ${PORT}. Add
proxy_set_header X-Forwarded-For $remote_addr; (nginx) or equivalent
so rate-limit attribution works.
OAuth flow
User clicks "Connect" in Claude Desktop / Coworks / custom MCP client
โ
GET /oauth/register (DCR โ client registers its redirect_uri)
โ
GET /auth/authorize?response_type=code
&client_id=<dcr-issued>
&redirect_uri=http://127.0.0.1:<port>/callback
&code_challenge=<S256(verifier)>
&code_challenge_method=S256
&state=<csrf-state>
โ server returns the HTML login form + a CSRF cookie
โ
POST /auth/login (user submits Redmine URL + API key)
โ server validates creds against /users/current.json on that URL
โ server mints a single-use auth code, 302s to redirect_uri?code=...&state=...
โ
POST /oauth/token (PKCE verification, code exchange)
โ server returns {access_token, refresh_token, token_type, expires_in}
โ
Every MCP tool call carries the access_token as Bearer
โ looked up in the token store โ Redmine API key extracted โ upstream call
โ
POST /oauth/token (grant_type=refresh_token) once the access token expires
โ old refresh_token consumed atomically (GETDEL in Redis), new pair issued
โ
POST /oauth/revoke (optional, on logout)The user only ever supplies their own Redmine credentials โ no second account is required on the MCP server side.
Adding the server to an MCP client
Claude Desktop, Claude Code, Coworks, โฆ
Settings โ Connectors โ Add custom connector โ URL =
https://your-server/mcp.
OAuth Client ID and Secret can be left blank โ the server is a public
OAuth client (token_endpoint_auth_method: none). The real credential
is the user's Redmine API key, captured during /auth/login.
From the Anthropic API or other MCP SDKs
Use the standard MCP streamable-http transport pointed at
https://your-server/mcp. The SDK handles DCR + PKCE automatically.
Tools implemented
All 53 tools accept structured arguments and return raw Redmine JSON
(or {} for 204 No Content responses). Tools that mutate state
require their respective Redmine role/permission. Destructive delete_*
tools require confirm=True.
Category | Tools |
Projects |
|
Issues |
|
Users |
|
Time entries |
|
Memberships |
|
Groups |
|
Versions |
|
Custom fields |
|
Queries |
|
limit parameters are clamped to 1..100 (Redmine's hard server-side
max). create_issue_relation.relation_type, create_version.status,
and create_version.sharing use typing.Literal so the MCP tool
schema surfaces the allowed values to the LLM.
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโ
โ MCP client โ Claude Desktop / Coworks / custom SDK
โ (loopback callback) โ
โโโโโโโโโโโโฌโโโโโโโโโโโโ
โ HTTPS
โโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ FastAPI app (server.py) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ RequestIDMiddleware โ contextvar request_id โ โ
โ โ SecurityHeadersMiddleware โ HSTS, CSP, X-Frame-Options, โฆ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ auth/routes.py โ โ 53 MCP tools (FastMCP) โ โ
โ โ /auth/authorize โ โ list_issues, create_project, โฆ โ โ
โ โ /auth/login โ โ call _redmine() โ outbound httpx โ โ
โ โ /oauth/token โ โ with SSRF re-validation + audit โ โ
โ โ /oauth/revoke โ โ โ โ
โ โ /oauth/register โ โ โ โ
โ โโโโโโโโโโโโโโโฌโโโโโโโโโโโโ โโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ โ
โ โโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ auth/token_store.py โ TokenStore Protocol โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ InMemoryTokenStore โ OR โ RedisTokenStore โ โ โ
โ โ โ (default) โ โ (Fernet-encrypted API keys) โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ audit.py โ one-line JSON to stdout โ โ
โ โ login_ok / login_rejected / token_issued / token_refreshed / โ โ
โ โ token_revoked / rate_limit_exceeded / redmine_call โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ httpx, follow_redirects=False
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ User's Redmine instance โ
โ X-Redmine-API-Key: <secret> โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโKey invariants:
API keys are wrapped in
RedactedStr(returns'***'fromrepr/str); they only reach the outbound HTTP path via.reveal().Every outbound URL is re-validated against the SSRF block-list on every call, not just at login.
The CSRF cookie is
HttpOnly,SameSite=Lax,Secure(unlessREDMINE_MCP_ALLOW_HTTP=true), path-scoped to/auth/.Audit records never contain API keys, query params, request bodies, or response bodies โ only method, path, status, latency, and the operator's Redmine login.
Security
SECURITY.md documents the threat model, every
defect found during the Phase 0 review, the fix, and the regression
test that pins it.
Vulnerabilities should be reported privately to the maintainer rather than as a public GitHub issue.
Testing
pip install -r requirements.txt
pip install pytest pytest-asyncio respx
pytestCurrently 60 tests covering Phase 0 + Phase 1 (~2s on a laptop) plus 2 opt-in Redis tests, run with:
docker run --rm -d -p 6379:6379 --name redis-test redis:7-alpine
REDMINE_MCP_TEST_REDIS_URL=redis://localhost:6379/15 pytest
docker stop redis-testRoadmap
TODO.md tracks remaining work. Phase 0 (critical
security) and Phase 1 (high-priority hardening) are complete. Phase 2
(reliability + code quality: modularization, shared httpx client,
retries, CI, ruff/mypy) and Phase 3 (operability: Prometheus metrics,
pydantic-settings, multi-stage Dockerfile) are the next chunks.
License
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/zh/redmine_mcp_py'
If you have feedback or need assistance with the MCP directory API, please join our Discord server