Redmine MCP Server
by Asad-hashmi
README.md
# Redmine MCP Server
Production-ready [Model Context Protocol](https://modelcontextprotocol.io/) server that exposes Redmine issue management to AI clients (Cursor, Claude Desktop, etc.) over **HTTP + SSE**, so your whole team can connect remotely.
## Features
- **18 Redmine tools**: issues, search, projects, statuses, create/update, time tracking, wiki pages
- **SSE transport** for remote team access (`GET /sse`, `POST /messages`)
- **Team gate** via `x-api-key` (`TEAM_SECRET` on server)
- **Per-user Redmine access** via `x-redmine-api-key` in each colleague’s Cursor `mcp.json`
- **Docker** and **docker-compose** for staging/production
- **nginx** sample config with SSE-friendly proxy settings
- **Health endpoint** for monitoring
## Project structure
```
redmine-mcp/
├── index.js # MCP server + Express app
├── package.json
├── .env # Local secrets (not committed)
├── .env.example
├── .gitignore
├── Dockerfile
├── docker-compose.yml
├── nginx.conf # Reverse proxy template
└── README.md
```
## Local setup
### Prerequisites
- Node.js 18+
- A Redmine instance with REST API enabled
- A Redmine API key (My account → API access key)
### Install
```bash
cd redmine-mcp
cp .env.example .env
# Edit .env with your Redmine URL and team secret (no personal API keys on the server)
npm install
```
### Configure `.env`
| Variable | Description |
|----------|-------------|
| `REDMINE_URL` | Shared Redmine base URL for the whole team |
| `TEAM_SECRET` | Shared gate secret; clients send as `x-api-key` |
| `PORT` | HTTP port (default `3456`) |
Each user’s **personal Redmine API key** is **not** stored on the server. They put it in Cursor MCP headers as `x-redmine-api-key`.
### Run locally
```bash
npm start
```
Verify:
```bash
curl -s http://localhost:3456/health
curl -s -H "x-api-key: YOUR_TEAM_SECRET" http://localhost:3456/health
```
## MCP tools
| Tool | Description |
|------|-------------|
| `get_my_issues` | Issues assigned to the API user (id, subject, status, priority, project, description, due_date, estimated_hours) |
| `get_issue` | Full issue by ID, including subtasks/children when available |
| `get_subtasks` | Child issues of a parent (`parent_issue_id`) |
| `search_issues` | Search by subject keyword; optional `project_id` |
| `get_project_issues` | Fetch all issues for a project by identifier slug |
| `get_projects` | List all projects |
| `get_issue_statuses` | List workflow statuses |
| `update_issue` | Update notes, `done_ratio`, `status_id` |
| `create_issue` | Create issue (`project_id`, `subject` required; optional description, priority, assignee) |
| `log_time` | Log hours on an issue; sets billable hours CF (defaults to `hours`); `activity_id` defaults to 5 |
| `get_time_entries` | Time entries for one issue |
| `get_my_time_entries` | Current user’s time entries (optional date range) |
| `get_time_activities` | List activity types and IDs for `log_time` |
| `get_project_time_entries` | Project time entries with per-user hour summary |
| `get_wiki_pages` | List wiki pages for a project |
| `get_wiki_page` | Fetch one wiki page by project and title |
| `create_wiki_page` | Create a wiki page in a project |
| `update_wiki_page` | Update an existing wiki page |
## Project Issue Tools
### `get_project_issues`
**Purpose:** Fetch all issues for a project using the project identifier slug.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `project_slug` | string | yes | Redmine project identifier slug |
| `status_id` | number or string | no | Status filter (default `*` for all statuses) |
| `include_subprojects` | boolean | no | Include subproject issues (default false) |
This tool follows Redmine pagination and returns all matching issues, not just the first page.
**Example prompts:**
- “Show all issues for project redmine”
- “Fetch all open issues for project my-project-slug”
- “List every issue in project support including subprojects”
## Time Tracking Tools
Use these tools to log work hours and review time spent in Redmine. Call **`get_time_activities`** first if you need valid `activity_id` values for **`log_time`**.
### `log_time`
**Purpose:** Log time spent on a Redmine issue.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `issue_id` | number | yes | Issue to log time on |
| `hours` | number | yes | Hours spent (must be > 0), e.g. `1.5` |
| `activity_id` | number | no | Activity type ID (default `5` = Development) |
| `comments` | string | no | Worklog comments |
| `spent_on` | string | no | Date `YYYY-MM-DD` (defaults to today) |
| `billable_hours` | number | no | Billable hours custom field (defaults to `hours`) |
Billable hours are sent as Redmine custom field `1` by default (`BILLABLE_HOURS_CF_ID` in `.env` if your instance uses another ID).
**Example prompts:**
- “Log 2.5 hours on issue #142 for development activity”
- “Log 1 hour on issue 98, activity_id 9, comments: code review”
- “Log 3 hours on issue 50 with 2 billable hours”
### `get_time_entries`
**Purpose:** Fetch all time entries for a specific issue.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `issue_id` | number | yes | Issue ID |
| `limit` | number | no | Max results (default `25`) |
Returns: `id`, `issue_id`, `project`, `user`, `activity`, `hours`, `comments`, `spent_on`, `created_on`.
**Example prompts:**
- “Show me all time entries for issue #98”
- “How much time has been logged on issue 142?”
### `get_my_time_entries`
**Purpose:** Fetch time entries logged by the connected Redmine user.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `from_date` | string | no | Start date `YYYY-MM-DD` |
| `to_date` | string | no | End date `YYYY-MM-DD` |
| `limit` | number | no | Max results (default `50`) |
**Example prompts:**
- “What have I logged this week?”
- “Show my time entries from 2026-05-12 to 2026-05-18”
### `get_time_activities`
**Purpose:** List available time activity types (Development, Design, Testing, etc.) and their IDs.
No parameters.
**Example prompts:**
- “What activity types are available for time logging?”
- “List Redmine time entry activities so I can log time”
### `get_project_time_entries`
**Purpose:** Fetch time entries for an entire project, with a summary grouped by user and total hours.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `project_id` | number or string | yes | Project numeric ID or identifier slug |
| `from_date` | string | no | Start date `YYYY-MM-DD` |
| `to_date` | string | no | End date `YYYY-MM-DD` |
| `limit` | number | no | Max results (default `50`) |
**Example prompts:**
- “Show all time entries for project redmine slug”
- “Summarize logged hours by user for project 5 this month”
## Wiki Tools
Use these tools to read and manage Redmine project wiki pages. `project_id` can be either the numeric project ID or the project identifier slug.
### `get_wiki_pages`
**Purpose:** List wiki pages for a project.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `project_id` | number or string | yes | Project numeric ID or identifier slug |
**Example prompts:**
- “List wiki pages for project redmine”
- “Show all wiki pages in project 5”
### `get_wiki_page`
**Purpose:** Fetch a wiki page by project and title.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `project_id` | number or string | yes | Project numeric ID or identifier slug |
| `title` | string | yes | Wiki page title |
| `include_attachments` | boolean | no | Include attachments when available |
**Example prompts:**
- “Show the Installation wiki page for project redmine”
- “Get the API Documentation wiki page with attachments”
### `create_wiki_page`
**Purpose:** Create a wiki page in a project.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `project_id` | number or string | yes | Project numeric ID or identifier slug |
| `title` | string | yes | Wiki page title |
| `text` | string | yes | Wiki page content |
| `comments` | string | no | Version comment |
| `parent_title` | string | no | Parent wiki page title |
**Example prompts:**
- “Create a wiki page called Deployment Notes in project redmine”
- “Create a child wiki page under Installation with these setup steps”
### `update_wiki_page`
**Purpose:** Update an existing wiki page.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `project_id` | number or string | yes | Project numeric ID or identifier slug |
| `title` | string | yes | Wiki page title |
| `text` | string | yes | Replacement wiki page content |
| `comments` | string | no | Version comment |
| `parent_title` | string | no | Parent wiki page title |
**Example prompts:**
- “Update the Deployment Notes wiki page in project redmine”
- “Replace the API Documentation wiki page content and add a version comment”
## Cursor `mcp.json` (team members)
Add to `~/.cursor/mcp.json` (or project `.cursor/mcp.json`).
- **`x-api-key`** — same `TEAM_SECRET` for everyone (from admin).
- **`x-redmine-api-key`** — each person’s own key from Redmine → My account → API access key.
```json
{
"mcpServers": {
"redmine": {
"url": "https://mcp.yourcompany.com/sse",
"headers": {
"x-api-key": "shared_team_secret_from_admin",
"x-redmine-api-key": "your_personal_redmine_api_key"
}
}
}
}
```
For local development:
```json
{
"mcpServers": {
"redmine-local": {
"url": "http://localhost:3456/sse",
"headers": {
"x-api-key": "shared_team_secret_from_admin",
"x-redmine-api-key": "your_personal_redmine_api_key"
}
}
}
}
```
`get_my_issues` and other tools use **the connected user’s** Redmine identity, not an admin key on the server.
## Health check
```bash
curl -s http://localhost:3456/health | jq
```
Returns server name, version, Redmine URL (configured), active SSE session count, and uptime. No API key required on `/health`.
## Staging deployment
### Option A — Docker (recommended)
```bash
cp .env.example .env
# fill in production values
docker compose up -d --build
docker compose logs -f
```
### Option B — PM2 on the host
```bash
npm install --omit=dev
npm install -g pm2
pm2 start index.js --name redmine-mcp
pm2 save
pm2 startup
```
### nginx reverse proxy
1. Copy `nginx.conf` to `/etc/nginx/sites-available/redmine-mcp`
2. Replace `mcp.yourcompany.com` with your domain
3. Enable site and reload nginx
4. Run certbot for TLS: `certbot --nginx -d mcp.yourcompany.com`
Ensure SSE locations keep `proxy_buffering off` and long `proxy_read_timeout`.
## API endpoints
| Method | Path | Auth | Purpose |
|--------|------|------|---------|
| GET | `/sse` | `x-api-key` + `x-redmine-api-key` | Establish MCP SSE stream (user key bound to session) |
| POST | `/messages?sessionId=...` | `x-api-key` | Client JSON-RPC messages (uses session’s Redmine key) |
| GET | `/health` | none | Liveness / status |
## Security notes
- Rotate `TEAM_SECRET` if leaked; share only via your team secret manager
- Terminate TLS at nginx; do not expose port 3456 publicly without a proxy
- Each colleague uses their own Redmine API key in Cursor; never commit keys to git
- Rotate `TEAM_SECRET` if leaked; rotate personal Redmine keys if compromised
## License
ISC
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues