Complete documentation for all available Redmine MCP Server tools.
The Redmine MCP Server supports comprehensive SSL/TLS configuration for secure connections to your Redmine instance.
Recommended Practices:
-
Always Use HTTPS
# In .env file REDMINE_URL=https://redmine.company.com # Use https://, not http://
-
Enable SSL Verification (Default)
- SSL verification is enabled by default for security
- Never disable in production environments
- Only disable for development/testing when absolutely necessary
-
Self-Signed Certificates
For Redmine servers with self-signed certificates or internal CA infrastructure:
# In .env file REDMINE_SSL_CERT=/path/to/ca-certificate.crtSecurity Considerations:
- Verify certificate authenticity before trusting
- Obtain certificates from trusted administrators
- Use absolute paths for certificate files
- Ensure certificate files have appropriate permissions (644)
-
Mutual TLS (Client Certificates)
For high-security environments requiring client certificate authentication:
# In .env file REDMINE_SSL_CLIENT_CERT=/path/to/cert.pem,/path/to/key.pemSecurity Considerations:
- Private keys MUST be unencrypted (Python requests library requirement)
- Store private keys securely with restricted permissions (600)
- Never commit certificates or keys to version control
- Rotate client certificates regularly per security policy
-
Development vs Production
⚠️ Development Only:REDMINE_SSL_VERIFY=false # WARNING: Only for development/testing!Disabling SSL verification makes your connection vulnerable to man-in-the-middle attacks. Never use in production.
-
API Key Authentication (Recommended)
Prefer API key authentication over username/password:
# In .env file REDMINE_API_KEY=your_api_key_hereBenefits:
- More secure than password storage
- Can be revoked without changing password
- Granular access control
- Better audit trail
-
Username/Password Authentication
Only use when API key is not available:
# In .env file REDMINE_USERNAME=your_username REDMINE_PASSWORD=your_passwordSecurity Considerations:
- Never commit credentials to version control
- Use strong, unique passwords
- Rotate passwords regularly
- Consider using API keys instead
-
Credential Storage
- Store credentials in
.envfile (not in code) - Add
.envto.gitignore - Use environment variables in production
- Consider using secret management systems (e.g., HashiCorp Vault, AWS Secrets Manager)
- Store credentials in
The server implements multiple security layers for file operations:
-
Server-Controlled Storage
- Attachment storage location controlled by server (
ATTACHMENTS_DIR) - Clients cannot specify arbitrary file paths
- Prevents directory traversal attacks
- Attachment storage location controlled by server (
-
UUID-Based File Storage
- Files stored with UUID-based names, not original filenames
- Prevents path manipulation and collision attacks
- Predictable cleanup and management
-
Time-Limited Access
- Download URLs expire based on server configuration
- Default expiry: 60 minutes (configurable via
ATTACHMENT_EXPIRES_MINUTES) - Automatic cleanup of expired files
-
Secure File Serving
- Metadata validation before file access
- Expiry checks on every request
- No directory listing or browsing
When deploying with Docker, follow these additional practices:
-
Certificate Management
# In docker-compose.yml volumes: - ./certs:/certs:ro # Read-only mount
-
Environment Variable Security
- Use separate
.env.dockerfile - Never include credentials in Dockerfile
- Use Docker secrets for sensitive data in production
- Use separate
-
Network Security
- Separate internal binding from external URLs
- Use reverse proxy (nginx, traefik) for SSL termination
- Restrict container network access
Block all write operations by setting the REDMINE_MCP_READ_ONLY environment variable:
# In .env file
REDMINE_MCP_READ_ONLY=trueWhen enabled, the following tools return an error instead of executing (write actions only — read actions within the same tool still work):
Fully blocked (all actions are writes):
create_redmine_issueupdate_redmine_issuedelete_redmine_issuecopy_issueupload_filedelete_fileimport_time_entriesupdate_checklist_item(also requiresREDMINE_CHECKLISTS_ENABLED=true)create_checklist_item(also requiresREDMINE_CHECKLISTS_ENABLED=true)manage_project_member— all actionsmanage_issue_watcher— all actionsmanage_issue_note— all actionsmanage_time_entry— all actionsmanage_redmine_version— all actions (create,update,delete)
Partially blocked (read actions still work):
manage_redmine_wiki_page—create,update,delete,renameblocked;list,getallowedmanage_issue_category—create,update,deleteblocked;listallowedmanage_issue_relation—create,deleteblocked;listallowedmanage_product—create,updateblocked;list,getallowed (also requiresREDMINE_PRODUCTS_ENABLED=true)manage_contact—create,update,delete,assign_to_project,remove_from_projectblocked;list,getallowed (also requiresREDMINE_CRM_ENABLED=true)manage_document—create,updateblocked;list,getallowed (also requiresREDMINE_DMSF_ENABLED=true)
All read tools (get_redmine_issue, list_redmine_issues, list_redmine_projects, etc.) continue to work normally. The admin-gated cleanup_attachment_files tool (when registered via REDMINE_MCP_EXPOSE_ADMIN_TOOLS=true) is also unaffected — it performs local filesystem cleanup, not Redmine mutations.
All user-controlled content returned from Redmine (issue descriptions, journal notes, wiki page text, search excerpts, version descriptions) is automatically wrapped in unique boundary tags:
<insecure-content-a1b2c3d4e5f67890>
User-controlled content here...
</insecure-content-a1b2c3d4e5f67890>
This allows LLM consumers to distinguish trusted tool output from untrusted user data, preventing prompt injection attacks via Redmine content. Empty strings and non-string values are returned unchanged.
- SSL Certificate Configuration - Detailed configuration examples
- Troubleshooting Guide - SSL Errors - Common SSL issues and solutions
- Environment Variables - Complete configuration reference
Lists all accessible projects in the Redmine instance.
Parameters: None
Returns: List of project dictionaries with id, name, identifier, and description
Example:
[
{
"id": 1,
"name": "My Project",
"identifier": "my-project",
"description": "Project description"
}
]List issue custom fields configured for a project, including allowed values and tracker bindings.
Parameters:
project_id(integer or string, required): Project ID (numeric) or identifier (string)tracker_id(integer, optional): Restrict output to fields applicable to the given tracker ID
Returns: List of custom field metadata dictionaries
Example:
[
{
"id": 6,
"name": "Size",
"field_format": "list",
"is_required": false,
"multiple": false,
"default_value": "M",
"possible_values": ["S", "M", "L"],
"trackers": [{"id": 5, "name": "Bug"}]
}
]Example with tracker filter:
list_project_issue_custom_fields(project_id="pipeline", tracker_id=5)is_required caveat (#119): the underlying GET /custom_fields.json only exposes the flag set on the custom field definition. Workflow rules, role-based field permissions, and tracker-bound required-field settings can still cause create_redmine_issue / update_redmine_issue to reject with "<field> cannot be blank" for a field that this tool returns with is_required: false. No general-purpose Redmine API exposes the "effective" required state.
When a create or update rejects with that error, the response envelope is augmented with missing_required_fields (parsed names) and a hint that lists the three recovery paths:
- Name-keyed shortcut — pass the rejected field by name directly:
fields={"Department": "Engineering"}on eithercreate_redmine_issueorupdate_redmine_issue. The tool resolves the name to acustom_fieldsid automatically. Ambiguous names (two fields normalized identically) raise. - Explicit id form —
extra_fields={"custom_fields": [{"id": N, "value": "..."}]}withNfrom this tool. Use when the name is ambiguous or the value type is awkward (multi-value, complex serializations). - Autofill — set
REDMINE_AUTOFILL_REQUIRED_CUSTOM_FIELDS=trueto have the server retry once with values from each field'sdefault_valueor theREDMINE_REQUIRED_CUSTOM_FIELD_DEFAULTSenv map.
Provide a comprehensive summary of project status based on issue activity over a specified time period.
Parameters:
project_id(integer, required): The ID of the project to summarizedays(integer, optional): Number of days to analyze. Default:30
Returns: Comprehensive project status summary including:
- Recent activity metrics (issues created/updated)
- Status, priority, and assignee breakdowns
- Project totals and overall statistics
- Activity insights and trends
Example:
{
"project_id": 1,
"project_name": "My Project",
"analysis_period_days": 30,
"recent_activity": {
"created_count": 15,
"updated_count": 42
},
"status_breakdown": {
"New": 5,
"In Progress": 8,
"Resolved": 12
}
}List versions (roadmap milestones) for a Redmine project. Useful for discovering target version IDs to use with list_redmine_issues(fixed_version_id=...).
Parameters:
project_id(integer or string, required): The project ID (numeric) or identifier (string)status_filter(string, optional): Filter by version status. Allowed values:open,locked,closed. Default: all versions
Returns: List of version dictionaries
Example:
[
{
"id": 1,
"name": "v1.0",
"description": "First release",
"status": "open",
"due_date": "2026-03-01",
"sharing": "none",
"wiki_page_title": "",
"project": {"id": 1, "name": "My Project"},
"created_on": "2026-01-01T10:00:00",
"updated_on": "2026-02-01T14:30:00"
}
]Usage with issue filtering:
# First, find versions for a project
versions = list_redmine_versions(project_id="my-project", status_filter="open")
# Then, list issues assigned to that version
issues = list_redmine_issues(fixed_version_id=versions[0]["id"])Create, update, or delete a Redmine version (roadmap milestone).
Parameters:
action(string, required): Operation to perform. Allowed values:create,update,deleteproject_id(integer or string): Project ID or identifier. Required foraction="create"version_id(integer): Version ID. Required foraction="update"andaction="delete"name(string): Version name. Required foraction="create", optional foraction="update"description(string, optional): Version descriptionstatus(string, optional): Version status. Allowed values:open,locked,closed. Defaults toopenon createdue_date(string, optional): Due date inYYYY-MM-DDformatsharing(string, optional): Sharing scope. Allowed values:none,descendants,hierarchy,tree,system. Defaults tononeon createwiki_page_title(string, optional): Associated wiki page title
Returns:
create/update: full version dictionary (same shape aslist_redmine_versionsentries)delete:{"success": true, "version_id": ..., "message": "Version deleted successfully."}- Error:
{"error": "..."}
Examples:
# Create a new version
manage_redmine_version(
action="create",
project_id="my-project",
name="v2.0",
description="Second major release",
status="open",
due_date="2026-09-01",
)
# Update version status to locked
manage_redmine_version(
action="update",
version_id=42,
status="locked",
)
# Delete a version
manage_redmine_version(
action="delete",
version_id=42,
)List all members (users and groups) of a Redmine project along with their assigned roles.
Parameters:
project_id(integer or string, required): Project ID (numeric) or identifier (string)
Returns: List of membership dictionaries containing user/group info and roles
Example:
[
{
"id": 1,
"user": {"id": 5, "name": "John Doe"},
"group": null,
"project": {"id": 10, "name": "My Project"},
"roles": [{"id": 3, "name": "Developer"}]
},
{
"id": 2,
"user": null,
"group": {"id": 15, "name": "Dev Team"},
"project": {"id": 10, "name": "My Project"},
"roles": [{"id": 4, "name": "Manager"}]
}
]Usage:
# List members by project ID
members = list_project_members(project_id=10)
# List members by project identifier
members = list_project_members(project_id="my-project")
# Get all developers in a project
devs = [m for m in members if any(r["name"] == "Developer" for r in m["roles"])]List all roles defined in the Redmine instance. Returns basic metadata (id and name) for each role.
Parameters: None.
Returns: List of role dictionaries with id and name.
Example:
[
{"id": 3, "name": "Manager"},
{"id": 4, "name": "Developer"},
{"id": 5, "name": "Reporter"}
]When to use: Call this before manage_project_member(action="add"|"update") to discover the correct role_ids. Role IDs vary between Redmine instances and must not be guessed — calling manage_project_member with a non-existent role ID returns a validation error from Redmine.
Retrieve the list of enabled modules for a project (e.g., issue_tracking, time_tracking, wiki, repository).
Parameters:
project_id(integer or string, required): Project identifier (numeric ID or string identifier).
Returns: Dictionary with project_id, project_name, and enabled_modules (list of module name strings).
Example:
{
"project_id": 1,
"project_name": "My Project",
"enabled_modules": ["issue_tracking", "time_tracking", "wiki"]
}Notes:
- Common module names:
issue_tracking,time_tracking,news,documents,files,wiki,repository,boards,calendar,gantt. - Plugins (Agile, CRM, etc.) may register additional modules; any plugin-provided module names are returned as-is.
Add, update, or remove a Redmine project membership.
Parameters:
action(string, required): Operation to perform. Allowed:add,update,removeproject_id(integer or string): Project identifier. Required foraction="add"membership_id(integer): Membership ID. Required foraction="update"andaction="remove"user_id(integer): ID of the user. Exactly one ofuser_idorgroup_idrequired foraction="add"group_id(integer): ID of the group. Exactly one ofuser_idorgroup_idrequired foraction="add"role_ids(array of integers): Non-empty list of role IDs. Required foraction="add"andaction="update". Uselist_redmine_rolesto discover valid IDs
Returns:
add/update: membership dictionary (withid,user/group,project,roles)remove:{"success": true, "deleted_membership_id": <id>}- Error:
{"error": "..."}
Examples:
# Add a user as Developer
manage_project_member(action="add", project_id="my-project", user_id=5, role_ids=[3])
# Add a group with multiple roles
manage_project_member(action="add", project_id=10, group_id=20, role_ids=[3, 4])
# Update an existing membership's roles
manage_project_member(action="update", membership_id=42, role_ids=[3, 4])
# Remove a membership
manage_project_member(action="remove", membership_id=42)Notes:
- All actions are write operations and respect
REDMINE_MCP_READ_ONLY. - Inherited memberships (from a parent project) cannot be removed directly — Redmine returns a 422. Remove them from the parent project instead.
- Redmine's API uses the
user_idfield for both users and groups; the tool routesgroup_idthrough that field automatically.
Retrieve detailed information about a specific Redmine issue.
Parameters:
issue_id(integer, required): The ID of the issue to retrieveinclude_journals(boolean, optional): Include journals (comments) in result. Default:trueinclude_attachments(boolean, optional): Include attachments metadata. Default:trueinclude_custom_fields(boolean, optional): Include custom fields in result. Default:truejournal_limit(integer, optional): Maximum number of journals to return. When set, enables journal pagination and addsjournal_paginationmetadata. Default:null(all journals)journal_offset(integer, optional): Number of journals to skip (used withjournal_limit). Default:0include_watchers(boolean, optional): Include watcher list. Default:falseinclude_relations(boolean, optional): Include issue relations. Default:falseinclude_children(boolean, optional): Include child issues. Default:false
Returns: Issue dictionary with details, journals, and attachments. Standard fields include category, fixed_version (target version), and parent (each {id, ...} or None), plus start_date, due_date, closed_on (ISO-8601 or None), done_ratio, estimated_hours, spent_hours, and is_private. Each is None when not set on the issue. When REDMINE_AGILE_ENABLED=true, also includes story_points, agile_sprint_id, and agile_position from the RedmineUP Agile plugin.
Attachment URLs (#110, #118): each entry under attachments carries the canonical shape {id, filename, filesize, content_type, description, content_url, author, created_on} — identical to what manage_redmine_wiki_page(action="get", include_attachments=True) returns. When REDMINE_PUBLIC_URL is set, any content_url whose scheme+host+port matches REDMINE_URL's origin is rewritten to use the public origin (preserving path, query, fragment, and any reverse-proxy subpath). When unset, the raw URL Redmine echoes back is returned — callers can fall back to get_redmine_attachment for a sandbox-safe download URL via the MCP server's proxy.
Example:
{
"id": 123,
"subject": "Bug in login form",
"description": "<insecure-content-...>\nUsers cannot login...\n</insecure-content-...>",
"status": {"id": 1, "name": "New"},
"priority": {"id": 2, "name": "Normal"},
"category": {"id": 5, "name": "Backend"},
"fixed_version": {"id": 6, "name": "v2.0"},
"parent": {"id": 100},
"start_date": "2026-01-10",
"due_date": "2026-01-20",
"closed_on": null,
"done_ratio": 40,
"estimated_hours": 8.0,
"spent_hours": 3.5,
"is_private": false,
"custom_fields": [{"id": 6, "name": "Size", "value": "S"}],
"journals": [...],
"attachments": [...]
}With REDMINE_AGILE_ENABLED=true:
{
"id": 123,
"subject": "Bug in login form",
"story_points": 5,
"agile_sprint_id": null,
"agile_position": 2,
...
}With REDMINE_TAGS_ENABLED=true:
{
"id": 123,
"subject": "Bug in login form",
"tags": [{"id": 3, "name": "fast-track"}, {"id": null, "name": "backend"}],
...
}Journal pagination:
get_redmine_issue(123, journal_limit=5, journal_offset=10)
# Returns:
# {
# ...
# "journals": [...], # 5 journals starting from position 10
# "journal_pagination": {
# "total": 42,
# "offset": 10,
# "limit": 5,
# "count": 5,
# "has_more": true
# }
# }Include watchers, relations, and children:
get_redmine_issue(
123,
include_watchers=True,
include_relations=True,
include_children=True
)
# Returns:
# {
# ...
# "watchers": [{"id": 10, "name": "Alice"}, {"id": 11, "name": "Bob"}],
# "relations": [{"id": 5, "issue_id": 123, "issue_to_id": 456, "relation_type": "relates"}],
# "children": [{"id": 200, "subject": "Sub-task", "tracker": {"id": 1, "name": "Bug"}}]
# }Notes:
- User-controlled content (
description, journalnotes) is wrapped in<insecure-content-{boundary}>boundary tags to prevent prompt injection - Journal pagination metadata is only included when
journal_limitis set - Watchers, relations, and children default to
falsefor backward compatibility
List Redmine issues with flexible filtering and pagination support. A general-purpose tool for listing issues from Redmine. Supports filtering by project, status, assignee, tracker, priority, and any other Redmine issue filter.
Parameters:
project_id(integer or string, optional): Filter by project (numeric ID or string identifier)status_id(integer, optional): Filter by status IDtracker_id(integer, optional): Filter by tracker IDassigned_to_id(integer or string, optional): Filter by assignee. Use a numeric user ID or the special value'me'to retrieve issues assigned to the currently authenticated user. Note that'me'resolves to the owner of the configuredREDMINE_API_KEY, which may be a shared or robot account rather than the human operator. If results come back unexpectedly empty, callget_mcp_server_infoto confirm who'me'maps to.priority_id(integer, optional): Filter by priority IDfixed_version_id(integer, optional): Filter by target version/milestone IDsort(string, optional): Sort order (e.g.,"updated_on:desc")limit(integer, optional): Maximum issues to return. Default:25, Max:1000offset(integer, optional): Number of issues to skip for pagination. Default:0include_pagination_info(boolean, optional): Return structured response with metadata. Default:falsefields(array of strings, optional): List of field names to include in results. Default: all fields- Available fields:
id,subject,description,project,status,priority,tracker,author,assigned_to,created_on,updated_on—trackeris returned by default - Special values:
["*"]or["all"]for all fields
- Available fields:
Returns: List of issue dictionaries, or structured response with pagination metadata
Examples:
List all issues in a project:
list_redmine_issues(project_id="my-project")Filter by multiple criteria:
list_redmine_issues(
project_id=1,
status_id=1,
assigned_to_id="me",
sort="updated_on:desc"
)With pagination metadata:
list_redmine_issues(
project_id=1,
limit=25,
offset=50,
include_pagination_info=True
)
# Returns:
# {
# "issues": [...],
# "pagination": {
# "total": 150,
# "limit": 25,
# "offset": 50,
# "has_next": true,
# "has_previous": true,
# "next_offset": 75,
# "previous_offset": 25
# }
# }With field selection (reduces token usage):
list_redmine_issues(
project_id=1,
fields=["id", "subject", "status"]
)
# Returns: [{"id": 1, "subject": "Bug fix", "status": {...}}, ...]Search issues using text queries with support for pagination, field selection, and native Search API filters.
Parameters:
query(string, required): Text to search for in issueslimit(integer, optional): Maximum number of issues to return. Default:25, Max:1000offset(integer, optional): Number of issues to skip for pagination. Default:0include_pagination_info(boolean, optional): Return structured response with pagination metadata. Default:falsefields(array of strings, optional): List of field names to include in results. Default:null(all fields)- Available fields:
id,subject,description,project,status,priority,tracker,author,assigned_to,created_on,updated_on—trackeris returned by default - Special values:
["*"]or["all"]for all fields
- Available fields:
scope(string, optional): Search scope. Default:"all"- Values:
"all","my_project","subprojects"
- Values:
open_issues(boolean, optional): Search only open issues. Default:false
Returns:
- By default: List of issue dictionaries
- With
include_pagination_info=true: Dictionary withissuesandpaginationkeys
When to Use:
- Use
search_redmine_issues()for text-based searches across issues - Use
list_redmine_issues()for advanced filtering by project_id, status_id, priority_id, etc.
Search API Limitations:
The Search API supports text search with scope and open_issues filters only. For advanced filtering by specific field values (project_id, status_id, priority_id, etc.), use list_redmine_issues() instead.
Examples:
Basic search:
search_redmine_issues("bug fix")With pagination:
# First page
search_redmine_issues("performance", limit=10, offset=0)
# Second page
search_redmine_issues("performance", limit=10, offset=10)With pagination metadata:
search_redmine_issues(
"security",
limit=25,
offset=0,
include_pagination_info=True
)
# Returns:
# {
# "issues": [...],
# "pagination": {
# "limit": 25,
# "offset": 0,
# "count": 25,
# "has_next": true,
# "has_previous": false,
# "next_offset": 25,
# "previous_offset": null
# }
# }With field selection (token reduction):
# Minimal fields for better performance
search_redmine_issues("urgent", fields=["id", "subject", "status"])With native filters:
# Search only in my projects for open issues
search_redmine_issues(
"bug",
scope="my_project",
open_issues=True
)All features combined:
search_redmine_issues(
"critical",
scope="my_project",
open_issues=True,
limit=10,
offset=0,
fields=["id", "subject", "priority", "status"],
include_pagination_info=True
)Performance Tips:
- Use pagination (default limit: 25) to prevent token overflow
- Use field selection to minimize data transfer and token usage
- Combine pagination + field selection for optimal performance
- Token reduction: ~95% fewer tokens with minimal fields vs all fields
Creates a new issue in the specified project. Blocked when REDMINE_MCP_READ_ONLY=true.
Parameters:
project_id(integer, required): Target project IDsubject(string, required): Issue subject/titledescription(string, optional): Issue description. Default:""fields(object|string, optional): Additional Redmine fields as:- an object (
{"priority_id": 3, "tracker_id": 1}), or - a serialized JSON object string (for MCP clients that pass string payloads)
- an object (
extra_fields(object|string, optional): Additional Redmine fields as:- an object (
{"priority_id": 3, "tracker_id": 1}), or - a serialized JSON object string
- an object (
uploads(list, optional): Files to attach to the issue. Maximum 10 items. Each item is an object with:- Exactly ONE source key:
content_base64(string): Raw file bytes encoded as base64.filenameis required when using this source.source_url(string): HTTP(S) URL the server fetches. Filename is derived from the URL orContent-Dispositionif omitted.file_path(string): Absolute path to a file already on the server. Must be insideATTACHMENTS_DIRor a directory listed inREDMINE_MCP_UPLOAD_FILE_ROOTS. Filename is derived from the path if omitted.
filename(string, optional): Name the attachment will have in Redmine. Required forcontent_base64; derived for other sources when omitted.content_type(string, optional): MIME type override (e.g."application/pdf").description(string, optional): Human-readable description for the attachment.
- Exactly ONE source key:
Returns: Created issue dictionary. When uploads is provided and at least one attachment succeeds, the response includes:
attachments(list): Metadata for each attached file (id, filename, filesize, content_url, etc.).journal_id(integer or null): ID of the journal entry the attachments were placed on, or null when no journal note accompanies the upload (i.e. when nonotesis provided).
Name-keyed custom fields (#123): fields accepts custom-field names directly. The tool resolves the name to a custom_fields entry via list_project_issue_custom_fields and rewrites the payload before sending it to Redmine. Ambiguous names (two custom fields that normalize to the same name) raise with an explicit error pointing at the id form.
# Name-keyed -- the tool resolves "Department" to its custom_field id
create_redmine_issue(
project_id=1,
subject="...",
fields={"Department": "Engineering"},
)
# Explicit id form -- use when names collide or value types are awkward
create_redmine_issue(
project_id=1,
subject="...",
extra_fields={"custom_fields": [{"id": 2, "value": "Engineering"}]},
)Validation-error envelope (#119): when Redmine rejects with "<field> cannot be blank" / "is not included in the list" / "is invalid", the returned envelope includes:
missing_required_fields(list of parsed field names)hint— a tailored recovery message that branches on whether the missing fields look like standard Redmine fields (Subject / Priority / Tracker / etc.) or custom fields, calling out theis_requiredcaveat for the latter (seelist_project_issue_custom_fieldsfor the underlying limitation).
Tags (REDMINE_TAGS_ENABLED=true): pass a tag_list in fields to tag the new issue via the AlphaNodes additional_tags plugin — a list of names (["fast-track", "backend"]) or a comma-separated string ("fast-track, backend"). It is extracted before custom-field resolution (so it never collides with a same-named custom field) and requires create_issue_tags (to mint new tag names) or edit_issue_tags (existing tags only). Silently ignored when the flag is off.
# Requires REDMINE_TAGS_ENABLED=true
create_redmine_issue(
project_id=1,
subject="...",
fields={"tag_list": ["fast-track", "backend"]},
)Autofill retry: if REDMINE_AUTOFILL_REQUIRED_CUSTOM_FIELDS=true is set and Redmine returns relevant custom-field validation errors, the server fetches project custom fields, auto-fills missing/invalid required custom fields from Redmine default_value or REDMINE_REQUIRED_CUSTOM_FIELD_DEFAULTS, and retries once.
Examples:
# Create a bug report
create_redmine_issue(
project_id=1,
subject="Login button not working",
description="The login button does not respond to clicks",
fields={"priority_id": 3, "tracker_id": 1}
)
# Create an issue with a file attached (from a server-side path)
create_redmine_issue(
project_id=1,
subject="Performance report attached",
uploads=[
{
"file_path": "/app/attachments/report.pdf",
"content_type": "application/pdf",
"description": "Q2 performance report"
}
]
)
# Create an issue with a file attached (from base64 content)
create_redmine_issue(
project_id=1,
subject="Screenshot of error",
uploads=[
{
"content_base64": "<base64-encoded-bytes>",
"filename": "error-screenshot.png",
"content_type": "image/png"
}
]
)Updates an existing issue with the provided fields. Blocked when REDMINE_MCP_READ_ONLY=true.
Parameters:
issue_id(integer, required): ID of the issue to updatefields(object, required): Dictionary of fields to updateuploads(list, optional): Files to attach to the issue. Maximum 10 items. Each item is an object with:- Exactly ONE source key:
content_base64(string): Raw file bytes encoded as base64.filenameis required when using this source.source_url(string): HTTP(S) URL the server fetches. Filename is derived from the URL orContent-Dispositionif omitted.file_path(string): Absolute path to a file already on the server. Must be insideATTACHMENTS_DIRor a directory listed inREDMINE_MCP_UPLOAD_FILE_ROOTS. Filename is derived from the path if omitted.
filename(string, optional): Name the attachment will have in Redmine. Required forcontent_base64; derived for other sources when omitted.content_type(string, optional): MIME type override (e.g."application/pdf").description(string, optional): Human-readable description for the attachment.
- Exactly ONE source key:
Returns: Updated issue dictionary. When uploads is provided and at least one attachment succeeds, the response includes:
attachments(list): Metadata for each attached file (id, filename, filesize, content_url, etc.).journal_id(integer): ID of the journal entry the attachments were placed on.
Note: You can use either status_id or status_name in fields. When status_name is provided, the tool automatically resolves the corresponding status ID.
You can also update custom fields by name (for example {"size": "S"}) and the tool will resolve them to Redmine custom_fields entries using project custom-field metadata. You can still pass explicit custom_fields with field IDs.
When REDMINE_AGILE_ENABLED=true, you can also set RedmineUP Agile fields, written via the Agile plugin endpoint:
story_points— non-negative integer, ornullto clear.agile_sprint_id— the sprint (board) the issue belongs to;0/nullremoves it from its sprint.position— position within the sprint/backlog (read back asagile_position; accepted under either name).
Each may be given top-level in fields or nested under an agile_data_attributes dict (both forms are equivalent):
# Move issue to sprint 117 (top-level or nested are equivalent)
update_redmine_issue(issue_id=123, fields={"agile_sprint_id": 117})
update_redmine_issue(issue_id=123, fields={"agile_data_attributes": {"agile_sprint_id": 117}})
# Remove from its sprint
update_redmine_issue(issue_id=123, fields={"agile_sprint_id": 0})Updates are applied in place: the tool reads the current agile_data row and carries the existing values (and row id) forward, so changing one agile field (e.g. the sprint) does not clear the others. The updated issue is returned with the resulting story_points, agile_sprint_id, and agile_position so the change can be verified from the response. If the plugin is disabled, these agile keys are silently ignored.
An agile_data_attributes value that is not an object, or that carries a key outside the three writable fields above, returns an error listing what was accepted rather than being ignored. If the current agile_data row cannot be read (other than it not existing yet), the write is aborted instead of falling back to a payload that would replace the row.
Note: these agile keys are always intercepted before custom field resolution, regardless of the REDMINE_AGILE_ENABLED setting. If your Redmine instance has a custom field literally named "story_points" (or "agile_sprint_id"/"position"), it cannot be updated by name through this tool — use explicit custom_fields with its field ID instead (e.g. {"custom_fields": [{"id": 42, "value": "8"}]}).
When REDMINE_TAGS_ENABLED=true, you can pass tag_list to set the issue's AlphaNodes additional_tags tags — a list of names or a comma-separated string; [] clears all tags. It replaces the full tag set (it is not additive), is intercepted before custom-field resolution (so a custom field named "tag_list" must use the explicit custom_fields id form), and requires create_issue_tags (new tags) or edit_issue_tags (existing tags only). A tag_list change is recorded in the issue journal. Silently ignored when the flag is off.
# Requires REDMINE_TAGS_ENABLED=true — replaces the issue's tags
update_redmine_issue(issue_id=123, fields={"tag_list": ["fast-track", "backend"]})
# Clear all tags
update_redmine_issue(issue_id=123, fields={"tag_list": []})Attaching files to a journal note: pass a notes key inside fields alongside uploads. The attachments will be associated with that journal note in the issue history.
Examples:
# Update issue status using status name
update_redmine_issue(
issue_id=123,
fields={
"status_name": "Resolved",
"notes": "Fixed the issue"
}
)
# Or use status_id directly
update_redmine_issue(
issue_id=123,
fields={
"status_id": 3,
"assigned_to_id": 5
}
)
# Update custom field by name
update_redmine_issue(
issue_id=123,
fields={
"size": "S"
}
)
# Set story points (requires REDMINE_AGILE_ENABLED=true)
update_redmine_issue(
issue_id=123,
fields={
"story_points": 8
}
)
# Attach a file from a URL and post a note referencing it
update_redmine_issue(
issue_id=123,
fields={
"notes": "Attached the latest test report"
},
uploads=[
{
"source_url": "http://localhost:3012/attachments/report-uuid",
"filename": "test-report.pdf",
"content_type": "application/pdf"
}
]
)
# Attach a server-side file (from ATTACHMENTS_DIR or REDMINE_MCP_UPLOAD_FILE_ROOTS)
update_redmine_issue(
issue_id=123,
fields={},
uploads=[
{
"file_path": "/app/attachments/screenshot.png"
}
]
)Hard-delete an issue via DELETE /issues/{id}.json. Blocked when REDMINE_MCP_READ_ONLY=true.
Issue deletion in Redmine is irreversible and cascades to subtasks, journals (comments), attachments, time entries, and inbound relations from issues that referenced this one. To prevent accidental destruction, the tool refuses unless confirm_delete=True, and refuses again when the issue has subtasks unless confirm_delete_with_children=True is also passed.
For other lifecycle operations, use create_redmine_issue, update_redmine_issue, copy_issue, or get_redmine_issue.
Parameters:
issue_id(integer, required): ID of the issue to delete. Must be a positive integer.confirm_delete(boolean, optional): WhenFalse(default), the tool refuses and returns an impact preview. PassTrueto actually delete.confirm_delete_with_children(boolean, optional): When the issue has subtasks,confirm_delete=Truealone refuses with codeCHILDREN_PRESENT. Pass this flag too to opt in to cascade-deleting the subtasks.
Refusal envelope (default):
{
"error": "Refusing to delete issue 42 without explicit confirmation.",
"code": "CONFIRMATION_REQUIRED",
"hint": "Issue deletion in Redmine is irreversible ...",
"impact": {
"issue_id": 42,
"subject": "...",
"children_count": 0,
"journals_count": 0,
"attachments_count": 0,
"relations_count": 0,
"time_entries_count": 0
}
}Success envelope:
{
"success": true,
"deleted_issue_id": 42,
"cascade_deleted": {
"issue_id": 42,
"subject": "...",
"children_count": 0,
"journals_count": 0,
"attachments_count": 0,
"relations_count": 0,
"time_entries_count": 0
}
}Error codes: CONFIRMATION_REQUIRED, CHILDREN_PRESENT, NOT_FOUND (with upstream_status: 404).
Examples:
# Preview what would be deleted
delete_redmine_issue(issue_id=42)
# -> {"code": "CONFIRMATION_REQUIRED", "impact": {...}, ...}
# Explicit delete
delete_redmine_issue(issue_id=42, confirm_delete=True)
# -> {"success": True, "deleted_issue_id": 42, ...}
# Subtasks present -> double-confirm
delete_redmine_issue(issue_id=42, confirm_delete=True)
# -> {"code": "CHILDREN_PRESENT", ...}
delete_redmine_issue(
issue_id=42,
confirm_delete=True,
confirm_delete_with_children=True,
)
# -> {"success": True, "cascade_deleted": {"children_count": 3, ...}}Duplicate an existing issue using Redmine's native copy mechanism. Optionally overrides selected fields, recursively copies subtasks, and copies attachments.
Parameters:
issue_id(integer, required): ID of the source issue to copy.project_id(integer or string, optional): Target project for the copy. Defaults to the source issue's project.subject(string, optional): New subject for the copy. Defaults to the source subject.link_original(boolean, optional): Create acopied_to/copied_fromrelation between the original and the copy. Default:true.copy_subtasks(boolean, optional): Recursively copy the source's subtasks. Default:true.copy_attachments(boolean, optional): Copy attachments to the new issue. Default:true.field_overrides(object or JSON string, optional): Field values to override on the copy (e.g.,{"assigned_to_id": 5, "description": "..."}).
Returns: Dictionary containing the newly created issue. On failure, a dict with an "error" key.
Example:
copy_issue(
issue_id=123,
subject="Copy of login bug",
field_overrides={"assigned_to_id": 7}
)Notes:
- Respects
REDMINE_MCP_READ_ONLY— returns an error in read-only mode. - The target user must have permission to create issues in the destination project.
List, create, or delete a Redmine issue relation.
Parameters:
action(string, required): Operation to perform. Allowed:list,create,deleteissue_id(integer): Source issue ID. Required foraction="list"andaction="create"issue_to_id(integer): Target issue ID. Required foraction="create"relation_id(integer): Relation ID. Required foraction="delete"relation_type(string, optional): One ofrelates,duplicates,duplicated,blocks,blocked,precedes,follows,copied_to,copied_from. Defaults torelateson createdelay(integer, optional): Delay in days. Only meaningful forprecedes/follows
Returns:
list: array of relation dicts (id,issue_id,issue_to_id,relation_type,delay)create: relation dictdelete:{"success": true, "deleted_relation_id": <id>}- Error:
{"error": "..."}
Examples:
# List all relations on an issue
manage_issue_relation(action="list", issue_id=123)
# Create a "blocks" relation
manage_issue_relation(action="create", issue_id=123, issue_to_id=456, relation_type="blocks")
# Create a "precedes" relation with a 3-day delay
manage_issue_relation(action="create", issue_id=1, issue_to_id=2, relation_type="precedes", delay=3)
# Delete a relation by ID
manage_issue_relation(action="delete", relation_id=42)Notes:
listis allowed in read-only mode;createanddeleteare blocked whenREDMINE_MCP_READ_ONLY=true.
List subtasks (child issues) of a given issue. Includes closed subtasks.
Parameters:
issue_id(integer, required): ID of the parent issue.
Returns: List of child issue dictionaries.
Notes:
- To create a new subtask, use
create_redmine_issuewith theparent_issue_idfield set:create_redmine_issue( project_id=1, subject="Subtask", fields={"parent_issue_id": 123} )
Add or remove a watcher on a Redmine issue. Requires Redmine 2.3.0+.
Parameters:
action(string, required): Allowed:add,removeissue_id(integer, required): ID of the issueuser_id(integer, required): ID of the user to add or remove
Returns: {"success": true, "issue_id": ..., "user_id": ...} on success; {"error": "..."} on failure.
Examples:
manage_issue_watcher(action="add", issue_id=123, user_id=5)
manage_issue_watcher(action="remove", issue_id=123, user_id=5)Notes:
- All actions are write operations and respect
REDMINE_MCP_READ_ONLY.
Edit text or toggle privacy of a Redmine journal entry (issue note). get_private_notes is a separate read tool.
Parameters:
action(string, required): Allowed:edit,set_privatejournal_id(integer, required): ID of the journal entry (fromget_redmine_issuewithinclude_journals=true)notes(string): New notes text (may be empty to clear). Required foraction="edit"private_notes(boolean, optional): Optionally toggle the private flag duringeditis_private(boolean): Required foraction="set_private"—trueto mark private,falseto make public
Returns:
edit:{"success": true, "journal_id": ..., "notes": ..., "private_notes": ...}set_private:{"success": true, "journal_id": ..., "private_notes": <bool>}- Error:
{"error": "..."}
Examples:
# Edit a note's text
manage_issue_note(action="edit", journal_id=42, notes="Updated text")
# Edit text and mark private at the same time
manage_issue_note(action="edit", journal_id=42, notes="Confidential", private_notes=True)
# Toggle privacy without changing the text
manage_issue_note(action="set_private", journal_id=42, is_private=True)Notes:
- Only the
notestext andprivate_notesflag can be edited. Journaldetails(field-change history) are immutable. - If a journal has no
detailsand its notes are cleared viaedit, Redmine will delete the journal record. - Both actions are writes and respect
REDMINE_MCP_READ_ONLY. - Requires
edit_issue_notes/edit_own_issue_notespermission (server-enforced).
Retrieve only the private notes (journals with private_notes=true) of an issue. Requires the "View private notes" permission.
Parameters:
issue_id(integer, required): ID of the issue.
Returns: List of journal dictionaries where private_notes is true. Journals with empty note bodies are omitted.
List, create, update, or delete a Redmine issue category.
Parameters:
action(string, required): Allowed:list,create,update,deleteproject_id(integer or string): Project identifier. Required foraction="list"andaction="create"category_id(integer): Category ID. Required foraction="update"andaction="delete"name(string): Category name. Required foraction="create", optional foraction="update"(cannot be blank)assigned_to_id(integer, optional): Default assignee user ID. Forcreateandupdatereassign_to_id(integer, optional): Reassign existing issues to this category ID ondelete. If omitted, issues become uncategorised
Returns:
list: array of category dicts (id,name,project,assigned_to)create/update: category dictdelete:{"success": true, "deleted_category_id": ..., "reassigned_to_id": ...}- Error:
{"error": "..."}
Examples:
# List all categories in a project
manage_issue_category(action="list", project_id="my-project")
# Create a new category with a default assignee
manage_issue_category(action="create", project_id=10, name="Backend", assigned_to_id=5)
# Rename a category
manage_issue_category(action="update", category_id=3, name="Renamed")
# Delete a category and reassign its issues to another one
manage_issue_category(action="delete", category_id=3, reassign_to_id=7)Notes:
listworks in read-only mode;create,update, anddeleteare blocked whenREDMINE_MCP_READ_ONLY=true.updaterequires at least one ofnameorassigned_to_id.
Render an interactive Kanban board of a project's issues (MCP Apps). Columns
are issue statuses; cards show id, subject, assignee, and priority. Drag a
card to another status column to change the issue's status in Redmine (writes
back via update_redmine_issue; the move reverts with an explanation if
Redmine rejects the transition). Dragging is disabled when
REDMINE_MCP_READ_ONLY=true. Requires a client that supports MCP Apps
rendering.
Parameters:
project_id(int | str, required): project to display.filters(dict, optional): extra Redmine filters, same aslist_redmine_issues.
Backend data source for the Kanban board's Refresh action. Returns the same
payload as show_triage_board without a UI resource. Called by the board's
iframe, not normally invoked directly.
Render a live project dashboard (health snapshot) for a project (MCP Apps):
open vs closed counts, overdue and due-this-week issues, an open-by-priority
breakdown, and recent activity. Clicking a figure drills into the matching
issue list in-panel. Prefer summarize_project_status when the user wants a
written narrative summary, and show_triage_board when they want to work
issues in a board. Read-only. Requires a client that supports MCP Apps
rendering.
Parameters:
project_id(int | str, required): project to display.filters(dict, optional): extra Redmine filters, same aslist_redmine_issues.
Backend data source for the dashboard's Refresh action. Returns the same
payload as show_project_dashboard without a UI resource. App-only; called
by the dashboard's iframe, not normally invoked directly.
List time entries from Redmine with optional filtering and pagination.
Parameters:
project_id(integer or string, optional): Filter by project (numeric ID or string identifier)issue_id(integer, optional): Filter by issue IDuser_id(integer or string, optional): Filter by user ID. Use"me"for current userfrom_date(string, optional): Start date filter (YYYY-MM-DD format)to_date(string, optional): End date filter (YYYY-MM-DD format)limit(integer, optional): Maximum entries to return. Default:25, Max:100offset(integer, optional): Number of entries to skip for pagination. Default:0
Returns: List of time entry dictionaries
Example:
[
{
"id": 1,
"hours": 2.5,
"comments": "Bug fix work",
"spent_on": "2024-03-15",
"user": {"id": 5, "name": "John Doe"},
"project": {"id": 10, "name": "My Project"},
"issue": {"id": 123},
"activity": {"id": 9, "name": "Development"},
"created_on": "2024-03-15T10:30:00",
"updated_on": "2024-03-15T10:30:00"
}
]Usage:
# List all time entries for a project
entries = list_time_entries(project_id="my-project")
# Filter by issue and date range
entries = list_time_entries(
issue_id=123,
from_date="2024-01-01",
to_date="2024-03-31"
)
# Get current user's time entries
my_entries = list_time_entries(user_id="me")Create or update a Redmine time entry. Replaces create_time_entry, update_time_entry, and log_time_for_user.
Parameters:
action(string, required): Allowed:create,updatehours(float): Hours spent. Required foraction="create"; optional forupdate(must be positive if provided)project_id(integer or string): Required foraction="create"ifissue_idis not providedissue_id(integer): Required foraction="create"ifproject_idis not provideduser_id(integer, optional): Log on behalf of this user (createonly). Requireslog_time_for_other_userspermissiontime_entry_id(integer): Entry ID to update. Required foraction="update"activity_id(integer, optional): Activity type (e.g., Development, Design)comments(string, optional): Description. Empty string clears the field onupdatespent_on(string, optional): Date inYYYY-MM-DDformat
Returns:
create/update: time entry dict (id,hours,comments,spent_on,user,project,issue,activity, etc.)- Error:
{"error": "..."}
Examples:
# Log 2.5h against an issue
manage_time_entry(action="create", hours=2.5, issue_id=123, comments="Fixed login bug")
# Log 1.0h against a project with explicit activity and date
manage_time_entry(
action="create",
hours=1.0,
project_id="my-project",
activity_id=9,
comments="Code review",
spent_on="2026-04-15",
)
# Log time on behalf of another user (replaces log_time_for_user)
manage_time_entry(
action="create",
hours=2.0,
issue_id=123,
user_id=7,
comments="Pair programming",
)
# Update an existing entry
manage_time_entry(action="update", time_entry_id=1, hours=3.0)Notes:
- Both actions are write operations and respect
REDMINE_MCP_READ_ONLY. - For bulk imports of multiple entries (timesheets, weekly reports), prefer
import_time_entriesover callingmanage_time_entry(action="create")in a loop. - Some Redmine versions reject
user_idwhen the authenticated admin is not a project member (Redmine defects #31587, #32774). The workaround is to add the admin as a project member.
List available time entry activity types from Redmine.
Use this tool to discover valid activity_id values before calling manage_time_entry.
When logging time against a specific project, always call with project_id first — project-specific activity IDs differ from global ones and using the wrong ID causes "Activity is not included in the list" errors.
Parameters:
project_id(string or integer, optional): Project identifier. When provided, returns project-specific activities viaGET /projects/:id.json?include=time_entry_activities(Redmine 3.4.0+).
Returns:
- Without
project_id: list of activity dicts withid,name,active,is_default - With
project_id: dict withproject_idandactivities(same structure). If the project has no custom activities,activitiesis empty and anotefield advises falling back to the global list.
Example (global):
[
{"id": 4, "name": "Development", "active": true, "is_default": false},
{"id": 5, "name": "Design", "active": true, "is_default": false},
{"id": 6, "name": "Testing", "active": true, "is_default": false}
]Example (project-scoped):
{
"project_id": "my-project",
"activities": [
{"id": 9, "name": "Development", "active": true, "is_default": false}
]
}These tools help LLMs discover valid IDs (trackers, statuses, priorities, users, saved queries) without guessing. Call these before create/update tools that require the corresponding ID.
List all trackers (issue types like Bug, Feature, Support). Use to discover valid tracker_id values.
Parameters: None.
Returns: List of {id, name, description} dicts.
Example:
[
{"id": 1, "name": "Bug", "description": ""},
{"id": 2, "name": "Feature", "description": "New feature requests"},
{"id": 3, "name": "Support", "description": ""}
]List trackers enabled for a specific project. Use this instead of list_redmine_trackers when you need only the trackers applicable to a given project (project settings can restrict the instance-wide tracker list).
Parameters:
project_id(integer or string, required): Project ID (numeric) or identifier (string)
Returns: List of {id, name} dicts for trackers enabled on the project.
Example:
[
{"id": 1, "name": "Bug"},
{"id": 2, "name": "Feature"}
]Usage:
# Discover which trackers a project accepts before creating an issue
trackers = list_project_trackers(project_id="my-project")
# [{"id": 1, "name": "Bug"}, {"id": 2, "name": "Feature"}]
# Then create an issue with a valid tracker_id
create_redmine_issue(project_id="my-project", subject="...", fields={"tracker_id": 1})Notes:
- Distinct from
list_redmine_trackers, which returns all trackers configured instance-wide regardless of project membership. Use this tool when the project is known and you want to avoid passing a tracker ID that Redmine will reject.
List all issue statuses. Use to discover valid status_id values. update_redmine_issue also accepts a status_name field that internally resolves the ID.
Parameters: None.
Returns: List of {id, name, is_closed} dicts — is_closed flags statuses that count as "closed" for reporting purposes.
Example:
[
{"id": 1, "name": "New", "is_closed": false},
{"id": 2, "name": "In Progress", "is_closed": false},
{"id": 5, "name": "Closed", "is_closed": true}
]List all priority levels. Use to discover valid priority_id values.
Parameters: None.
Returns: List of {id, name, active, is_default} dicts.
Example:
[
{"id": 1, "name": "Low", "active": true, "is_default": false},
{"id": 2, "name": "Normal", "active": true, "is_default": true},
{"id": 3, "name": "High", "active": true, "is_default": false}
]List Redmine users with optional filtering. Requires admin permission — non-admin users receive a 403 error. Use to discover user IDs for assignment, watchers, or time-entry authoring.
Parameters:
name(string, optional): Case-insensitive substring filter (matches login, firstname, lastname, email).group_id(integer, optional): Filter users who belong to a specific group.limit(integer, optional): Maximum users to return (default 25, clamped to 1–100).offset(integer, optional): Pagination offset. Default 0.
Returns: List of {id, login, firstname, lastname, mail, created_on} dicts.
Example:
list_redmine_users(name="alice")
# [{"id": 5, "login": "alice", "firstname": "Alice", "lastname": "A", ...}]Retrieve the currently authenticated user's profile. Resolves to GET /my/account.json, so works for any authenticated user (not admin-only). Useful when a user asks the LLM to do something "for me" — the LLM can call this to resolve the current user's ID.
Parameters: None.
Returns: Dict with id, login, firstname, lastname, mail, admin, created_on, last_login_on.
Example:
{
"id": 5,
"login": "alice",
"firstname": "Alice",
"lastname": "A",
"mail": "alice@example.com",
"admin": false,
"created_on": "2025-01-15T10:00:00",
"last_login_on": "2026-04-16T09:30:00"
}List all saved custom queries (saved issue filters) visible to the current user. Once discovered, the id can be passed as a query_id filter to list_redmine_issues to run the saved query.
Parameters: None.
Returns: List of {id, name, is_public, project_id} dicts. project_id is null for cross-project queries.
Example:
[
{"id": 1, "name": "Open bugs", "is_public": true, "project_id": 10},
{"id": 2, "name": "My tasks", "is_public": false, "project_id": null}
]Notes:
- Read-only tool. Redmine's REST API does not support creating, updating, or deleting saved queries — they can only be managed via the web UI.
Bulk-import multiple time entries via sequential API calls. Redmine has no native bulk-import endpoint, so each entry is POSTed individually. Per-entry errors are captured so a partial import still yields useful feedback.
Parameters:
entries(array of objects, required): List of time entry dicts. Each entry accepts:hours(required), plus at least one ofproject_id/issue_id. Optional:user_id(log on behalf of a teammate),activity_id,comments,spent_on. Capped at 500 entries per call -- split larger imports into multiple invocations. (The JSON-string variant was dropped in #114; passing a string is rejected at the FastMCP boundary with theINVALID_ARGUMENTSenvelope.)stop_on_error(boolean, optional): Abort on the first error. Default:false(continue past errors).
Returns: Dictionary with:
total(integer): total entries attemptedsucceeded(integer): count of successful entriesfailed(integer): count of failed entriescreated(array): successfully-created time entry dictserrors(array):{index, entry, error}for each failed entry
Example:
import_time_entries([
{"hours": 2.0, "issue_id": 123, "comments": "Bug fix"},
{"hours": 1.5, "project_id": "web", "activity_id": 9, "user_id": 7},
{"hours": 3.0, "issue_id": 456},
])
# Returns:
# {
# "total": 3,
# "succeeded": 3,
# "failed": 0,
# "created": [...],
# "errors": []
# }Partial failure example:
# One entry has invalid activity_id -> continues past it
import_time_entries([
{"hours": 1.0, "issue_id": 1},
{"hours": 2.0, "issue_id": 2, "activity_id": 999}, # bogus
{"hours": 3.0, "issue_id": 3},
])
# Returns:
# {
# "total": 3, "succeeded": 2, "failed": 1,
# "created": [<entry 1>, <entry 3>],
# "errors": [{"index": 1, "entry": {...}, "error": "Activity is invalid"}]
# }Notes:
- Unknown fields in each entry are silently filtered out (whitelist:
hours,user_id,project_id,issue_id,activity_id,comments,spent_on). - Respects
REDMINE_MCP_READ_ONLY.
Search across issues and wiki pages in the Redmine instance. Requires Redmine 3.3.0 or higher.
Parameters:
query(string, required): Text to search forresources(list, optional): Filter by resource types. Allowed:["issues", "wiki_pages"]. Default: both typeslimit(integer, optional): Maximum results to return (max 100). Default: 100offset(integer, optional): Pagination offset. Default: 0
Returns:
{
"results": [
{
"id": 123,
"type": "issues",
"title": "Bug in login page",
"project": "Web App",
"status": "Open",
"updated_on": "2025-01-15T10:00:00Z",
"excerpt": "First 200 characters of description..."
},
{
"id": null,
"type": "wiki_pages",
"title": "Installation Guide",
"project": "Documentation",
"status": null,
"updated_on": "2025-01-10T14:30:00Z",
"excerpt": "First 200 characters of wiki text..."
}
],
"results_by_type": {
"issues": 1,
"wiki_pages": 1
},
"total_count": 2,
"query": "installation"
}Example:
# Search all resource types
search_entire_redmine(query="installation guide")
# Search only wiki pages
search_entire_redmine(query="setup", resources=["wiki_pages"])
# With pagination
search_entire_redmine(query="bug", limit=25, offset=0)Notes:
- Requires Redmine 3.3.0+ for search API support
- v1.4 scope limitation: Only
issuesandwiki_pagessupported - Invalid resource types are silently filtered out
- Search is case-sensitive/insensitive based on Redmine server DB config
List, get, create, update, delete, or rename a Redmine wiki page. Replaces list_wiki_pages, get_redmine_wiki_page, create_redmine_wiki_page, update_redmine_wiki_page, delete_redmine_wiki_page, and rename_wiki_page.
Parameters:
action(string, required): Allowed:list,get,create,update,delete,renameproject_id(integer or string, required): Project identifier (numeric ID or short name)wiki_page_title(string): Wiki page title. Required for all actions exceptlistversion(integer, optional): Specific version number forget(default: latest)include_attachments(boolean, optional): Include attachment metadata ingetresponse. Default:truetext(string): Page content. Required forcreateandupdatecomments(string, optional): Change log comment forcreateandupdatenew_title(string): New title forrename(must differ fromwiki_page_title)redirect_existing_links(boolean, optional): Whentrue(default),renamecreates aWikiRedirectfrom the old title to the new title
Returns:
list: array of page metadata dicts (title,version,parent_titleif present,created_on,updated_on) — no body textget/create/update: full wiki page dict (title,text,version,created_on,updated_on,author,project,attachmentswhen applicable)delete:{"success": true, "title": ..., "message": ...}rename:{"success": true, ...}plus the renamed page's metadata- Error:
{"error": "..."}
Examples:
# List all wiki pages in a project (metadata only)
manage_redmine_wiki_page(action="list", project_id="my-project")
# Get the latest version of a page (with attachments by default)
manage_redmine_wiki_page(action="get", project_id="my-project", wiki_page_title="Installation_Guide")
# Get a specific version, no attachments
manage_redmine_wiki_page(
action="get",
project_id=123,
wiki_page_title="Installation_Guide",
version=3,
include_attachments=False,
)
# Create a new page
manage_redmine_wiki_page(
action="create",
project_id="my-project",
wiki_page_title="Getting_Started",
text="# Getting Started\n\nWelcome to the project!",
comments="Initial creation",
)
# Update an existing page
manage_redmine_wiki_page(
action="update",
project_id="my-project",
wiki_page_title="Installation_Guide",
text="# Installation\n\nUpdated steps...",
comments="Refreshed for 2026",
)
# Delete a page
manage_redmine_wiki_page(action="delete", project_id="my-project", wiki_page_title="Obsolete_Page")
# Rename a page (default: leaves a WikiRedirect)
manage_redmine_wiki_page(
action="rename",
project_id="my-project",
wiki_page_title="Old_Title",
new_title="New_Title",
)Notes:
listandgetare allowed in read-only mode;create,update,delete, andrenameare blocked whenREDMINE_MCP_READ_ONLY=true.- Wiki page titles typically use underscores instead of spaces.
- Content format (Textile/Markdown) depends on Redmine server configuration.
- Use
get_redmine_attachmentto download wiki attachments. renamerequires therename_wiki_pagespermission. If the API user lacks it, Redmine silently drops the title change (the body still updates). The tool re-fetches the page atnew_titleafter the update and returns an explicit error if the new title is unreachable.- The
renameaction implementsPUT /projects/{id}/wiki/{old_title}.jsonand fetches the existing text automatically since Redmine requirestexton every wiki update.
List all files uploaded to a Redmine project's Files section (not issue attachments).
Parameters:
project_id(integer or string, required): Project identifier.
Returns: List of file metadata dictionaries (id, filename, filesize, content_type, description, content_url, digest, downloads, author, version, created_on).
Example:
[
{
"id": 42,
"filename": "spec.pdf",
"filesize": 125678,
"content_type": "application/pdf",
"description": "Design spec v2",
"content_url": "https://redmine.example.com/attachments/download/42/spec.pdf",
"author": {"id": 5, "name": "Alice"},
"version": {"id": 3, "name": "Release 1.0"},
"created_on": "2026-04-10T10:30:00"
}
]Notes:
- Lists files from Redmine's core "Files" module (enabled per project via Settings > Modules > Files).
- Does NOT list issue attachments — use
get_redmine_issuewithinclude_attachments=Truefor those. - Does NOT list DMSF documents (separate module, not covered by this tool).
Upload a file to a Redmine project's Files section. Uses Redmine's standard two-step upload (POST /uploads.json for the token, then POST /projects/{id}/files.json).
Provide exactly ONE of source_url, content_base64, or file_path:
source_url(string) — the server downloads from an HTTP(S) URL. Use this when chaining from another MCP tool that returns a download URL (e.g., Google Drive MCP'sget_drive_file_download_url), or when the file is served by a local MCP onlocalhost. Preferred when a URL is available — no need for the caller to download and re-encode.content_base64(string) — raw file bytes encoded as base64. Use this only when the caller already has the bytes in memory.file_path(string): absolute path to a file already on the server. The path must be insideATTACHMENTS_DIRor a directory listed inREDMINE_MCP_UPLOAD_FILE_ROOTS. Filename is derived from the path iffilenameis omitted.
Parameters:
project_id(integer or string, required): Project identifier.filename(string, optional): Name the file should have in Redmine.- Required when using
content_base64. - Optional with
source_urlorfile_path, inferred from the URL path,Content-Dispositionheader, or file path if omitted, but always prefer passing an explicit filename.
- Required when using
source_url(string, conditional): HTTP(S) URL to download from.content_base64(string, conditional): File content as base64.file_path(string, conditional): Absolute path to a file on the server. Restricted toATTACHMENTS_DIRand directories inREDMINE_MCP_UPLOAD_FILE_ROOTS.description(string, optional): Human-readable description.version_id(integer, optional): Version/release ID to attach the file to (uselist_redmine_versionsto discover valid IDs).
Returns: Dictionary containing the uploaded file's metadata, or {"error": "..."} on failure.
Size limit: 50 MiB per file. Larger files should be uploaded via Redmine's web UI.
Examples:
# From a URL (chained from another MCP tool)
upload_file(
project_id="web",
source_url="http://localhost:3012/attachments/abc-123",
filename="report.pdf",
description="Q2 report"
)
# From base64 content
import base64
content = base64.b64encode(b"Hello world").decode("ascii")
upload_file(
project_id="web",
filename="hello.txt",
content_base64=content
)
# From a server-side file (must be in ATTACHMENTS_DIR or REDMINE_MCP_UPLOAD_FILE_ROOTS)
upload_file(
project_id="web",
file_path="/app/attachments/export.csv",
description="Latest data export"
)URL fetch details:
- Only
http://andhttps://schemes are supported. - Follows redirects automatically.
- 30-second timeout on the download.
- Streams the response and aborts early if the size exceeds 50 MiB.
Notes:
- Respects
REDMINE_MCP_READ_ONLY. - After successful upload, the tool re-fetches full metadata (filename, size, author, etc.) via
GET /attachments/{id}.json, since Redmine returns HTTP 204 on create with no body. file_pathaccess is restricted by the server to prevent path traversal. Only paths insideATTACHMENTS_DIRor explicitly allowed directories (REDMINE_MCP_UPLOAD_FILE_ROOTS) are permitted.
Delete a file from a Redmine project. Uses DELETE /attachments/{id}.json since files are stored as attachments in Redmine.
Parameters:
file_id(integer, required): ID of the attachment to delete (fromlist_files).confirm_delete_any_attachment(boolean, optional): Bypass the project-scope check to delete issue/wiki/news attachments. Default:false.
Returns: {"success": true, "deleted_file_id": <id>} on success.
Notes:
- Redmine's
DELETE /attachments/{id}.jsonremoves ANY attachment, not just project files. To prevent accidental deletion of issue attachments,delete_filefirst fetches the target and checks itscontainer_type. If it's notProject(e.g., it's anIssueorWikiPageattachment), the tool refuses and tells the caller how to bypass. - To intentionally delete an issue/wiki/news attachment via this tool, pass
confirm_delete_any_attachment=True. - Respects
REDMINE_MCP_READ_ONLY.
Download a Redmine attachment and return a usable reference to it. Works in both HTTP and stdio deployments without any configuration from the caller.
Parameters:
attachment_id(integer, required): The ID of the attachment to retrieve
Returns (HTTP mode — any explicit PUBLIC_HOST, or a non-loopback SERVER_HOST):
{
"uri": "http://my-server.example.com:8000/files/12345678-1234-5678-9abc-123456789012",
"uri_type": "http",
"filename": "document.pdf",
"content_type": "application/pdf",
"size": 1024,
"expires_at": "2026-05-09T14:00:00Z",
"attachment_id": 456
}Returns (stdio mode — neither PUBLIC_HOST nor a non-loopback SERVER_HOST set):
{
"file_path": "/absolute/local/path/uuid/document.pdf",
"uri_type": "file",
"filename": "document.pdf",
"content_type": "application/pdf",
"size": 1024,
"expires_at": "2026-05-09T14:00:00Z",
"attachment_id": 456
}Callers distinguish the two shapes via uri_type: "http" or "file". Per the #109 wrap policy, filename is structured metadata and is returned verbatim (not wrapped in <insecure-content> tags).
Mode detection (post-#105):
- An explicit
PUBLIC_HOST(any value, includinglocalhost/127.0.0.1) selects HTTP mode. This is the correct behavior for Docker port-forwarded deployments wherelocalhoston the host actually does reach the container's HTTP server. - Otherwise,
SERVER_HOSTis consulted; non-loopback values promote to HTTP mode. Loopback values (localhost,127.0.0.1,0.0.0.0) keep stdio mode becauseSERVER_HOSTis the bind address and0.0.0.0is not a reachable URL host. - Otherwise stdio mode is used and
file_pathis returned. - Port is resolved via
PUBLIC_PORT→SERVER_PORT→8000.
Note (#110): this tool returns its own MCP-proxy URL (HTTP mode) or local file path (stdio), so it is NOT affected by REDMINE_PUBLIC_URL. That env var rewrites content_url values returned by other tools (get_redmine_issue.attachments[*].content_url, list_files, etc.) from the internal Redmine origin to the configured public origin — see get_redmine_issue.
Security features:
- Downloads capped at
ATTACHMENT_MAX_DOWNLOAD_BYTES(default 200 MB). Exceeding the cap aborts the download and deletes the partial file. - Streaming download with byte counter — cap is enforced even when Redmine's reported file size is missing or understated.
- Filename sanitized to basename before writing to disk (path traversal protection).
- UUID-based storage directories prevent filename collisions and enumeration.
file_pathis always an absolute path (resolved viaPath.resolve()).- Files are cleaned up automatically by the background cleanup manager on the same schedule as other attachments.
Examples:
# stdio mode (no PUBLIC_HOST set) -- pass file_path to another tool
result = get_redmine_attachment(attachment_id=456)
if result["uri_type"] == "file":
# Pass directly to Claude Code's Read tool or pdf-mcp
content = read_file(result["file_path"])
else:
download_url = result["uri"]
# HTTP mode -- uri is a time-limited HTTP URL
result = get_redmine_attachment(attachment_id=456)
if result["uri_type"] == "http":
print(f"Download from: {result['uri']}")
print(f"Expires at: {result['expires_at']}")Operator tool, gated by REDMINE_MCP_EXPOSE_ADMIN_TOOLS=true (#115). Not registered on the default MCP surface — the background cleanup task already runs on the CLEANUP_INTERVAL_MINUTES schedule (default 10 min), so an LLM agent should almost never need to invoke this directly. Operators driving cleanup through the MCP surface set the flag to opt in; the underlying background cleanup runs regardless.
Removes expired attachment files and provides cleanup statistics.
Parameters: None
Returns: Cleanup statistics:
cleaned_files: Number of files removedcleaned_bytes: Total bytes cleaned upcleaned_mb: Total megabytes cleaned up (rounded)
Example:
{
"cleaned_files": 12,
"cleaned_bytes": 15728640,
"cleaned_mb": 15
}Note: Automatic cleanup runs in the background based on server configuration. This tool allows manual cleanup on demand.
These tools require the RedmineUP Checklists Pro plugin installed on your Redmine instance and REDMINE_CHECKLISTS_ENABLED=true.
Retrieve all checklist items for a Redmine issue.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
issue_id |
int | Yes | The ID of the issue whose checklist to retrieve |
Returns:
| Field | Type | Description |
|---|---|---|
issue_id |
int | The issue ID |
total_count |
int | Number of checklist items |
items |
list | Array of checklist item objects |
items[].id |
int | Checklist item ID |
items[].subject |
string | Item text (wrapped in <insecure-content> tags) |
items[].is_done |
bool | Whether the item is completed |
items[].is_section |
bool | Whether the item is a section header (not a checkable item) |
items[].position |
int | Position/order of the item |
items[].created_at |
string | ISO timestamp of creation |
items[].updated_at |
string | ISO timestamp of last update |
Error cases:
- Plugin disabled: returns
{"error": "Checklist support is disabled. Set REDMINE_CHECKLISTS_ENABLED=true..."} - Invalid
issue_id: returns{"error": "issue_id must be a positive integer."} - Issue not found / permission denied: returns Redmine API error
Update a checklist item's text, done state, or position. This is a write operation and is blocked in read-only mode.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
checklist_item_id |
int | Yes | The ID of the checklist item to update |
subject |
string | No | New text for the checklist item |
is_done |
bool | No | New done state |
position |
int | No | New position/order |
At least one optional parameter must be provided.
Returns:
| Field | Type | Description |
|---|---|---|
success |
bool | true on success |
checklist_item_id |
int | The updated item's ID |
updated_fields |
list | Names of fields that were updated |
Error cases:
- Read-only mode: returns read-only error
- Plugin disabled: returns plugin-disabled error
- No fields provided: returns
{"error": "No fields to update..."} - Invalid
is_donetype: returns{"error": "is_done must be a boolean."} - Invalid
position: returns{"error": "position must be a positive integer."}
Add a new checklist item (or section header) to a Redmine issue's checklist. This is a write operation and is blocked in read-only mode. Requires the RedmineUP Checklists Pro plugin and REDMINE_CHECKLISTS_ENABLED=true.
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
issue_id |
int | Yes | — | The ID of the issue to add the checklist item to |
subject |
string | Yes | — | Text of the new checklist item or section header |
is_section |
bool | No | false |
When true, creates a section header rather than a checkable item |
is_done |
bool | No | false |
Initial done state for checkable items (ignored when is_section=true) |
position |
int | No | null |
1-based position in the checklist. Omit to append at the end |
Returns:
| Field | Type | Description |
|---|---|---|
checklist_item_id |
int | ID of the newly created checklist item |
issue_id |
int | The issue ID |
subject |
string | Item text as stored |
is_done |
bool | Done state |
is_section |
bool | Whether the item is a section header |
position |
int or null | Position in the checklist |
Error cases:
- Read-only mode: returns read-only error
- Plugin disabled: returns
{"error": "Checklist support is disabled. Set REDMINE_CHECKLISTS_ENABLED=true..."} - Invalid
issue_id: returns{"error": "issue_id must be a positive integer."} - Blank
subject: returns{"error": "subject is required and cannot be blank."}
Examples:
# Append a checkable item
create_checklist_item(issue_id=123, subject="Write unit tests")
# Append a section header (organises items visually)
create_checklist_item(issue_id=123, subject="QA Phase", is_section=True)
# Insert a pre-completed item at position 2
create_checklist_item(issue_id=123, subject="Review spec", is_done=True, position=2)Retrieve project timeline (Gantt) data: issues with start/due dates, progress, dependencies, and milestones. No plugin required — uses core Redmine REST API (GET /issues.json with include=relations + GET /projects/{id}/versions.json).
Use cases: "What's the current timeline for project X?", "Which issues are overdue?", "Show me the dependency chain on this project". Returns structured data, not an image.
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
project_id |
int or string | Yes | – | Project identifier |
start_date_after |
string | No | – | YYYY-MM-DD filter (issues with start_date >= this) |
due_date_before |
string | No | – | YYYY-MM-DD filter (issues with due_date <= this) |
include_closed |
bool | No | false |
Include closed issues. Default false keeps response size and pagination cost low on long-lived projects; set to true for full historical timelines. |
limit |
int | No | 250 |
Max issues (1–500) |
Returns:
| Field | Type | Description |
|---|---|---|
project_id |
int/str | Echo of input |
total_count |
int | Number of issues returned |
issues |
list | Each item has id, subject, tracker, status, assigned_to, start_date, due_date, done_ratio, estimated_hours, parent_id, relations (only precedes/blocks) |
versions |
list | Milestones: id, name, due_date, status |
Note: If the API user lacks permission to view versions, the versions list falls back to empty rather than failing the entire call.
Performance: Redmine paginates issues at 25 per HTTP call by default, so a limit=500 request can trigger ~20 underlying API calls. Expect a few seconds of latency on large projects.
These tools require the RedmineUP Products plugin and REDMINE_PRODUCTS_ENABLED=true.
List, get, create, or update RedmineUP products. Replaces list_products, get_product, add_product, and edit_product.
Requires the RedmineUP Products plugin and REDMINE_PRODUCTS_ENABLED=true.
Parameters:
action(string, required): Allowed:list,get,create,updateproject_id(integer or string, optional): Forlist, filters products by project (omitted = all accessible). Forcreate, optionally associates the new product with a projectlimit(integer, optional): Forlist, max results per call (default100). Redmine capslimitat 100 server-side; values above are clampedproduct_id(integer): Required forgetandupdatename(string): Required forcreatestatus_id(integer, optional): Forcreate. Must be1(Active, default) or2(Inactive)description,code(string, optional): Forcreateprice(float, optional): Forcreatecurrency(string, optional): Forcreate(e.g.,"USD")category_id(integer, optional): Forcreatetag_list(string, optional): Forcreate, comma-separated tagscustom_fields(list, optional): Forcreate, list of{"id": N, "value": ...}dictsfields(dict): Forupdate, fields to update. Allowed keys:name,description,price,currency,status_id,code,project_id,category_id,tag_list,custom_fields. Unknown keys are silently filtered
Returns:
list: array of product dictsget/create: product dictupdate:{"success": true, "product_id": N, "updated_fields": [...]}- Error:
{"error": "..."}
Examples:
# List products in a project
manage_product(action="list", project_id="catalog", limit=50)
# Fetch a specific product
manage_product(action="get", product_id=42)
# Create a new product
manage_product(
action="create",
name="Widget Pro",
status_id=1,
project_id="catalog",
price=49.99,
currency="USD",
code="WP-001",
)
# Update a product's price
manage_product(action="update", product_id=42, fields={"price": 39.99})Notes:
listandgetare allowed in read-only mode;createandupdateare blocked whenREDMINE_MCP_READ_ONLY=true.- The plugin does not expose a delete endpoint, so
manage_producthas nodeleteaction.
These tools require the RedmineUP CRM plugin and REDMINE_CRM_ENABLED=true.
Security note: Contact PII (email, phone, address) is returned as-is to the caller but is never logged via this module's logger.
List, get, create, update, delete, or change project association for RedmineUP CRM contacts. Replaces list_contacts, get_contact, create_contact, edit_contact, delete_contact, assign_contact_to_project, and remove_contact_from_project.
Requires the RedmineUP CRM plugin and REDMINE_CRM_ENABLED=true. Visibility scoping is enforced server-side by Redmine.
Parameters:
action(string, required): Allowed:list,get,create,update,delete,assign_to_project,remove_from_projectproject_id(integer or string): Forlist, optional project filter. Forcreate, required (project to associate the new contact with). Forassign_to_project/remove_from_project, the project to attach to or detach fromsearch(string, optional): Forlist, free-text search (matches name/company/email)tags(string, optional): Forlist, comma-separated tag filterassigned_to_id(integer, optional): Forlist, filter by assignee user IDlimit(integer, optional): Forlist, max results per call (default100, capped at 100 by Redmine)contact_id(integer): Required for all actions exceptlistandcreateinclude(string, optional): Forget, comma-separated includes (notes,deals,contacts)first_name(string): Required forcreatelast_name,company,email,phone(string, optional): Forcreateis_company(boolean, optional): Forcreate.trueto mark as a company entity (defaultfalse)visibility(integer, optional): Forcreate.0=Project (default),1=Public,2=Privatefields(dict): Forupdate, fields to update. Allowed keys:first_name,last_name,middle_name,company,job_title,phone,email,website,skype_name,birthday,background,address_attributes,tag_list,is_company,assigned_to_id,custom_fields,visibility,project_id. Forcreate, additional fields beyond the named parameters
Returns:
list: array of contact dictsget/create: contact dictupdate:{"success": true, "contact_id": N, "updated_fields": [...]}delete:{"success": true, "contact_id": N, "message": ...}assign_to_project/remove_from_project:{"success": true, "contact_id": N, "project_id": ...}- Error:
{"error": "..."}
Examples:
# List contacts in a project, filtering by tag and assignee
manage_contact(
action="list",
project_id="sales",
tags="lead",
assigned_to_id=5,
limit=50,
)
# Fetch a single contact with related notes and deals
manage_contact(action="get", contact_id=42, include="notes,deals")
# Create a new contact
manage_contact(
action="create",
project_id="sales",
first_name="Alice",
last_name="Smith",
company="ACME",
email="alice@example.com",
visibility=0,
)
# Update a contact's job title
manage_contact(action="update", contact_id=42, fields={"job_title": "Director"})
# Delete a contact
manage_contact(action="delete", contact_id=42)
# Add a contact to an additional project (without creating a new record)
manage_contact(action="assign_to_project", contact_id=42, project_id="support")
# Remove a contact from a project (does not delete the contact)
manage_contact(action="remove_from_project", contact_id=42, project_id="support")Notes:
listandgetare allowed in read-only mode;create,update,delete,assign_to_project, andremove_from_projectare blocked whenREDMINE_MCP_READ_ONLY=true.- PII handling: contact
email,phone,address,birthday,websiteare returned as-is to the caller; the module never logs them. Error messages reference onlycontact_id. User-controlled display fields (first_name,last_name,middle_name,company,job_title,background,assigned_to.name) are wrapped in<insecure-content>boundary tags so downstream LLMs treat them as untrusted data.
This section requires the redmine_dmsf plugin (GPL v2, open-source) installed on the Redmine server, and REDMINE_DMSF_ENABLED=true. DMSF replaces Redmine's built-in (web-UI-only) Documents module with a full document-management system that exposes a REST API.
Combined DMSF CRUD tool. Action-dispatched — pass action="list"|"get"|"create"|"update".
Parameters (per action):
| Action | Required | Optional |
|---|---|---|
list |
project_id |
folder_id, limit (1–100) |
get |
document_id |
– |
create |
project_id, filename, content_base64 |
title, description, comment, folder_id, version, custom_fields |
update |
document_id, fields |
– |
Common parameter types:
project_id:intorstring(Redmine project identifier).folder_id,document_id: positiveint.filename:string. Used as the initial filename oncreate; can be changed later by passingnameinupdate'sfields(DMSF renames the parent file when a revision'snamediffers). Sent to DMSF as thenamekey insideattachments.uploaded_file— the upload helper readscommitted_file[:name], not[:filename].content_base64:string(raw file bytes encoded as base64). Decoded payload is capped at 50 MiB.version(forcreate): semantic version string for the new revision, e.g."1.0"or"1.2.3". Accepts"X","X.Y", or"X.Y.Z"; missing parts are padded with"0". Each part must be a non-negative integer. DMSF storesmajor_version/minor_version/patch_versionas separate integer columns; the tool splits on.before sending.custom_fields: list of{"id": N, "value": ...}dicts. Sent to DMSF ascustom_field_values(the API's internal key).fields(forupdate): dict with any subset of the writable keys:title,name(rename),description,comment,custom_fields. Unknown keys are silently filtered out.
Returns:
list: list of node dicts. Each node hasid,type(file/folder/file-link/folder-link),filename,title,name,description,version,size,content_type,folder_id,project_id,author({id, name}),created_on,updated_on.get: a single node dict with the same shape. Most metadata (description,size,version,mime_type,user_id, timestamps) is pulled from the latest entry ofdmsf_file_revisions[]— see the design notes below.create: a sparse dict containing onlyid+name(plus anotepointing ataction="get"for full metadata). DMSF's commit endpoint deliberately returns id + name only; callgetif you need description/size/version/timestamps. Returns{"success": True}if the response is unexpectedly empty.update:{"success": True, "document_id": N, "updated_fields": [...], "note": "DMSF created a new revision; previous revisions remain accessible via the document's revision history."}.- Any failure:
{"error": "..."}.
Examples:
# List documents in a project
manage_document(action="list", project_id="docs", limit=50)
# List documents inside a specific DMSF folder
manage_document(action="list", project_id="docs", folder_id=12)
# Get metadata for one document
manage_document(action="get", document_id=42)
# Upload a new document with an explicit initial version
import base64
content_b64 = base64.b64encode(open("spec.pdf", "rb").read()).decode()
manage_document(
action="create",
project_id="docs",
filename="spec.pdf", # sent to DMSF as `name` (the helper reads :name)
content_base64=content_b64,
title="Specification",
description="Initial draft",
comment="Created from CLI",
version="0.1", # split into version_major / version_minor / version_patch
)
# Update metadata (creates a new revision — DMSF is versioned)
manage_document(
action="update",
document_id=42,
fields={"title": "Specification v2", "description": "Updated draft"},
)
# Rename a document (DMSF assigns the revision's `name` back to the parent file)
manage_document(
action="update",
document_id=42,
fields={"name": "spec-v2.pdf"},
)DMSF design notes:
- Every update creates a new revision. DMSF is a versioned document system — there is no in-place mutation. Previous revisions remain accessible via the document's revision history in the Redmine UI.
- Required-field auto-population on
update. DMSF's revision-create controller crashes (NoMethodErroronnil.scrub) if eithertitleornameis missing. To makeupdateergonomic when the caller only wants to changedescription, the tool pre-fetches the current document viaGET /dmsf_files/{id}.jsonand uses the existingtitle/nameas defaults. The caller's overrides take precedence. - Renaming. Set
fields["name"]onupdateto rename the document; DMSF propagates the new name to the parent file. The list-shapefilenamefield cannot be used inupdate— only the canonicalnamekey. - Sparse
createresponse. The commit endpoint intentionally returns onlyid+name(plus atotal_count). For full metadata, follow up withaction="get"using the returnedid. - Two response shapes for the same document.
listreturns flat nodes;getnests most metadata underdmsf_file_revisions[]. The serializer merges both into one stable representation. - Version on
create, not onupdate. Pass a semanticversionstring (e.g."1.2.3") oncreateto set the initial revision's version. The tool splits the string intoversion_major/version_minor/version_patchand nests them insideattachments.uploaded_file(where DMSF's commit helper reads them).updatedoes not expose version control — DMSF auto-increments the patch version when a new revision is created viadmsf_files#create_revision. Use the Redmine web UI if you need explicit semantic version control on existing documents. (Why the asymmetry? DMSF's two endpoints read the version fields from different places:commitreads them nested insideuploaded_file, whilecreate_revisionreads them from top-levelparams. The MCP tool covers the more common case — version-at-upload-time.) - DMSF replaces the built-in Documents module rather than complementing it. If your Redmine instance has existing native documents, the server admin must run
rake redmine:dmsf_convert_documentsto migrate them before they're accessible here. - HTTP endpoint paths used (visible in raw error messages):
GET /projects/{id}/dmsf.json— listGET /dmsf_files/{id}.json— get (legacy path is preserved for show)POST /uploads.jsonthenPOST /projects/{id}/dmsf/commit.json— createPOST /dmsf/files/{id}/revision/create.json— update (note slash, not underscore:dmsf/files, notdmsf_files)
- If you see a 404 on these endpoints, the
redmine_dmsfplugin is not installed (or is too old to expose the REST API); double-check the server withbundle exec rake redmine:plugins:migrate RAILS_ENV=productionafter installation.
Notes:
listandgetare allowed in read-only mode;createandupdateare blocked whenREDMINE_MCP_READ_ONLY=true.- Per the #109 wrap policy,
descriptionis free-text and is wrapped in<insecure-content>boundary tags;filename,name,title, andauthor.nameare structured metadata and are returned verbatim.
Return the MCP server's version, enabled-feature flags, and the identity of the authenticated Redmine user. Use this tool to detect deployment lag (the running server may be behind a recently-shipped patch) before relying on a fix that landed on develop (compare server_version against the release / commit you expect), and to confirm who assigned_to_id="me" resolves to.
Parameters: None
Returns:
server_version(string): the deployed package version (fromimportlib.metadata). The literal"0.0.0+unknown"when the package metadata is unavailable (rare; source-tree runs without an editable install).read_only_mode(boolean): whetherREDMINE_MCP_READ_ONLYis enabled. WhenTrue, all write tools refuse with the standard read-only error.auth_mode(string):"oauth"or"legacy".current_user(dict or null):{id, login, name}for the authenticated Redmine user behind the configured API key.nullwhen the server cannot reach Redmine (check/healthfor connectivity status). Use this to confirm whoassigned_to_id="me"resolves to, which matters when a shared or robot API key is in use.plugin_flags(dict): which plugin-gated tool families are enabled. Keys:agile,checklists,products,crm,dmsf,tags.Truemeans the correspondingmanage_*/get_*tools are routable and will reach the underlying plugin endpoints (fortags, thatget_redmine_issuereturns atagsarray);Falsemeans they will return a "feature disabled" error envelope (or, fortags, that the field is omitted).
The response intentionally excludes credentials, internal hostnames, file-system paths, and any other operator config that a caller doesn't need to know to choose its call shape. Only flags that change call shape are surfaced.
Example:
{
"server_version": "1.3.0",
"read_only_mode": false,
"auth_mode": "legacy",
"current_user": {"id": 5, "login": "jdoe", "name": "Jane Doe"},
"plugin_flags": {
"agile": false,
"checklists": false,
"products": false,
"crm": false,
"dmsf": true,
"tags": false
}
}When to call:
- Before re-probing a recently-shipped fix to confirm the deployment has caught up.
- Before relying on a plugin-gated tool (
manage_contact,manage_product,manage_document,get_checklist, etc.) —plugin_flagstells you whether the call will succeed or return "feature disabled". - Before adapting to
auth_modeif your caller has different code paths for OAuth vs legacy. - When
list_redmine_issues(assigned_to_id="me")returns unexpectedly empty results:current_usershows the identity behind the configured API key, which may be a shared or robot account rather than the human operator.