Skip to main content
Glama
Asad-hashmi

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