All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
2.5.0 - 2026-07-04
create_redmine_issueandupdate_redmine_issuenow accept anuploadsparameter to attach files to an issue (and to a journal note when combined withnotes), resolving each file fromcontent_base64,source_url, or a new on-diskfile_pathsource.upload_filegains afile_pathsource for files already on the server.- New
REDMINE_MCP_UPLOAD_FILE_ROOTSsetting to allowlistfile_pathupload directories (defaults toATTACHMENTS_DIR). legacy-per-userauth mode: per-request Redmine API key via anX-Redmine-API-Keyheader, for Redmine instances too old for OAuth. Opt-in and fail-closed (REDMINE_PER_USER_TRUST_PROXYrequired); keys are redacted from logs; optional identity audit viaREDMINE_PER_USER_AUDIT_IDENTITY.trackerfield in issue serialization and thefieldsselector for issue listing/search tools.list_project_trackerstool for project-scoped tracker discovery.create_checklist_itemtool (RedmineUP Checklists) andis_sectionin checklist output.
2.4.0 - 2026-06-27
- Promotional demo page under
pages/, deployed to GitHub Pages on version tags via a newdeploy-demo.ymlworkflow. It is a scripted, client-side walkthrough of an AI agent triaging a sample Redmine sprint backlog (list, read, reassign, comment, log time, close), with tool-call request/response JSON that matches the server's real response shapes, a Kanban board that updates as the agent works, and a light/dark theme toggle. No live Redmine is connected.
get_redmine_issuenow returns journal field-changedetails(status, assignee, custom-field edits) and no longer drops journals that have no note text. The_journals_to_listhelper previously skipped any journal whosenoteswas empty viaif not notes: continueand never serialized thedetailsarray, so field-only history was lost anddetailswas missing even on journals with notes. Journals are now kept when they have a note or field-change details, and each entry includesdetails(property,name,old_value,new_value) plusprivate_notes.get_private_notesexposesdetailsas well. (#161)
- Add a direct
joserfc>=1.6.7,<2floor to clear CVE-2026-48990 (CWE-400 uncontrolled resource consumption).joserfc1.3.4 through 1.6.5 fails to applyJWSRegistry.max_payload_lengthto RFC 7797 unencoded (b64=false) JWS payloads, allowing oversized payloads to be deserialized; the fix landed in 1.6.6.joserfcis a transitive dependency viaauthlibandfastmcp, so the direct floor ensures the fix reaches PyPI installs, not only the pinned lockfile. The lock now resolvesjoserfc1.7.1. - Free-form journal field-change values now receive the same prompt-injection wrapping as journal notes. Custom-field values (
cf),description/subjectedits, and attachment filenames indetailsare wrapped in<insecure-content-{boundary}>tags, so the newly surfaced field-change history cannot smuggle injected instructions past an LLM consumer. Structured values (status, assignee, priority IDs, dates, numbers) are left raw to avoid bloating output with boundary tags. (#161)
test_scope_advertising_subset_of_sandbox_scopesnow asserts the introspected test token is active before checking scope overlap. A staleREDMINE_OAUTH_TEST_TOKENintrospects with an empty scope, which previously made the test fail with a misleading "name drift between oauth_scopes.py and live Doorkeeper config" message; the active-token guard surfaces the real cause (re-mint the bearer) instead.
- @martindglaser — fix missing journal field-change details in
get_redmine_issue(#163)
2.3.1 - 2026-06-20
- Bump
python-multipart0.0.29 to 0.0.32 to clear CVE-2026-53539 and CVE-2026-53538 (both fixed upstream in 0.0.30). The direct floor inpyproject.tomlis also raised from>=0.0.27to>=0.0.30so the fix reaches PyPI installs, not only the pinned lockfile. (#150) - Bump
starlette1.0.1 to 1.3.1 to clear four advisories: CVE-2026-48818 and CVE-2026-48817 (fixed in 1.1.0), CVE-2026-54282 (1.3.0), and CVE-2026-54283 (1.3.1).starletteis now also a declared direct dependency (the server imports it directly) with a>=1.3.1floor, so PyPI installs cannot resolve a vulnerable version. Dependabot does not propose transitive bumps on its own, so this was applied manually. (#162) - Bump
cryptography46.0.7 to 49.0.0 to clear GHSA-537c-gmf6-5ccf (fixed in 48.0.1).cryptographyis a transitive dependency viaauthlibandjoserfc; a direct>=48.0.1floor is added topyproject.tomlso the fix reaches PyPI installs. (#162)
- Remove the unused
fastapi[standard]dependency. The server is built directly on Starlette, FastMCP, and Uvicorn and never imported FastAPI, so dropping it removes a large unused transitive tree (typer,sentry-sdk,jinja2,uvloop,fastapi-cli,orjson,ujson, and others) from installs.starletteis now declared directly to keep the dependency it actually uses explicit. The PyPIChangelogproject URL now points at thedevelopbranch instead of a stalemasterpath. - Rewrite the package description to state what the server does ("MCP server that lets AI assistants manage Redmine issues, projects, wikis, and time tracking") instead of marketing adjectives, applied consistently across
pyproject.toml,server.json, and the GitHub repository description. - Update PyPI trove classifiers to match the project's status:
Development Statusmoves from4 - Betato5 - Production/Stable, andPython :: 3 :: Only,Bug Tracking,System Administrators,OS Independent, andWeb Environmentclassifiers are added for accuracy and discoverability. - Bump runtime dependencies
fastmcp3.3.1 to 3.4.2 (#152) anduvicorn0.48.0 to 0.49.0, which pullshttptools0.8.0 (#151). The OAuth discovery and introspection paths were smoke-tested under FastMCP 3.4.2. - Bump development and CI tooling:
pytest9.0.3 to 9.1.1 (#160),pytest-asyncio1.3.0 to 1.4.0 (#143),actions/checkoutto 6.0.3 (#142),astral-sh/setup-uvto 8.2.0 (#149), andcodecov/codecov-action6.0.1 to 7.0.0 (#148).
2.3.0 - 2026-06-12
- New opt-in authentication mode
REDMINE_AUTH_MODE=oauth-proxy, backed by FastMCP'sOAuthProxy. In this mode the MCP server is the OAuth authorization server that MCP clients talk to (serving Dynamic Client Registration plus/authorize,/token, and/register) and proxies the upstream flow to Redmine/Doorkeeper, keeping user consent on Redmine viarequire_authorization_consent="external". This supports clients that require DCR or CIMD without Redmine having to serve RFC 8414 metadata or implement DCR, which the existingoauth(introspection) mode could not provide on split-host deployments. The existing remote-auth setup is refactored into aRedmineAuthProvider,/healthnow probes Redmine introspection in bothoauthandoauth-proxymodes, and the app supports mounting behind a public base path viaREDMINE_MCP_BASE_URL. Token validation continues to use Doorkeeper introspection, and secrets may be supplied via*_FILEenv vars for Docker/Kubernetes. Legacy andoauthmodes are unchanged andlegacyremains the default; the new mode also requires a stableREDMINE_MCP_JWT_SIGNING_KEY. Seedocs/oauth-setup.mdfor setup. (#153) oauth-proxymode restricts client redirect URIs to loopback by default (http://localhost:*,http://127.0.0.1:*). Since MCP clients register their own redirect URI via DCR, this prevents a registered client from pointing the flow at a remote target out of the box. SetREDMINE_MCP_ALLOWED_CLIENT_REDIRECT_URISto a comma- or space-separated list of glob patterns for hosted clients, or*to accept any redirect URI.docs/oauth-setup.mdalso documents that OAuthProxy state is stored node-locally, so the mode is single-replica / sticky-session unless a sharedclient_storagebackend is configured.
- Secret
*_FILEenv vars (for exampleREDMINE_MCP_JWT_SIGNING_KEY_FILE) now raise a clear error naming the variable and path when the referenced file cannot be read, instead of surfacing a bareFileNotFoundError.docs/oauth-setup.mdalso notes that when the upstreamoauth-proxyclient falls back to the introspection client, that client must have the authorization code grant and the/auth/callbackredirect URI configured.
python tests/run_tests.py --allnow actually runs the integration suite. The test hermeticity guard introduced in 2.2.0 blanksREDMINE_*environment variables for any run that is not an explicit-m integrationinvocation, so the single unmarked--allcommand was treated as a unit run and silently skipped every integration test.--allnow runs the unit phase (-m "not integration") and the integration phase (-m integration) as two separate pytest processes so each gets the correct environment; integration coverage is appended. (#156)
docs/oauth-setup.mdStep 4 now shows the correct authorization-server metadata path per mode. Inoauth-proxymode the issuer isREDMINE_MCP_BASE_URL, so RFC 8414 metadata is served at the root/.well-known/oauth-authorization-server; the/mcp-suffixed path used byoauthmode 404s there and previously read as a failure during verification. (#140)
- @aadnehovda, designed and implemented the
oauth-proxyauthentication mode backed by FastMCPOAuthProxy, and refactored the remote OAuth setup intoRedmineAuthProvider(#153) - @timcomport, verified the
oauth-proxymode end to end on a split-host VS Code + Redmine 6.1.1 deployment and caught the Step 4 authorization-server metadata path discrepancy (#140)
2.2.0 - 2026-06-06
- Pin
pyjwt[crypto]>=2.13.0,<3to pull the fix for four advisories in 2.12.1: SSRF viaPyJWKClientnon-HTTP URL handlers (CVE-2026-48522), unbounded JWKS fetches driven by an unverifiedkidheader (CVE-2026-48524), incorrect detached-JWS (b64:false) payload decoding (CVE-2026-48525), and HMAC/asymmetric algorithm confusion where an issuer public key is accepted as the HMAC secret (CVE-2026-48526).pyjwtis a transitive dependency viamcp, so it had no direct floor before this. Verified the 2.13.0 release is published by pyjwt maintainer Jose Padilla.
get_mcp_server_infonow returnscurrent_user({id, login, name}) for the authenticated Redmine user, ornullwhen Redmine is unreachable. This lets a caller see whoassigned_to_id="me"resolves to, which matters when a shared or robot API key is in use and"me"is not the human operator. Thelist_redmine_issuesassigned_to_iddocs now point toget_mcp_server_infowhen"me"queries return unexpectedly empty. (#139)/healthin legacy mode now probesGET /users/current.jsonto verify the configured credentials and reports the result underchecks.redmine:"ok"when accepted,"unreachable"withstatus: "degraded"on auth failure, and"unconfigured"withstatus: "ok"when no URL or credentials are set. Auth misconfiguration now surfaces at the health check instead of only on the first failed tool call. The response stays HTTP 200 so orchestrators keep treating it as a binary liveness probe. (#139)- Docker images are now built and published to the GitHub Container Registry (
ghcr.io/jztan/redmine-mcp-server) on each release. The release workflow builds multi-architecture images (linux/amd64,linux/arm64) and applies the version tags only after the matching PyPI wheel publishes, so a release never produces an image without its wheel. Pull:latest, a pinned:X.Y.Z, or a minor series:X.Y. Requested by @Bricklou (#141).
create_redmine_issueno longer returns the bare"Requested resource not found."message when the create request gets an HTTP 404. A 404 on a create POST is anomalous: it generally comes from the deployment or from Redmine itself rather than a genuinely missing resource (for example a sub-URI/Passenger deployment, a reverse proxy, or a plugin or controller filter on the create path), so Redmine can process the POST and create the issue while the client still sees a 404. The bare message invited blind retries and risked silent duplicate issues. The tool now returns a message explaining the issue may have been created and advising the caller to check Redmine before retrying. (#146)scripts/release.pynow recognizes comma-separated contributor entries (- @user, did X) when building the GitHub release notes. The author-parsing regex previously accepted only colon, hyphen, en dash, and em dash separators, so comma-style entries failed to parse and the generated## Acknowledgementsblock came out empty. This silently dropped the Contributors credits from the v2.0.1 and v2.1.0 GitHub releases (the CHANGELOG entries themselves were unaffected); both releases have been backfilled.- OAuth mode: the
/.well-known/oauth-authorization-server/mcpdiscovery document now reportsissueras the Redmine URL instead of the MCP server's own base URL. On a split-host deployment (REDMINE_URLandREDMINE_MCP_BASE_URLon different hosts), the previousissuerdisagreed withauthorization_serversin the protected-resource document, so a spec-strict client (e.g. VS Code 1.122.1) treated the MCP server as the authorization server and requested/authorizeon the MCP host, which 404s. The issuer now matchesauthorization_serversand the endpoint URLs, all naming Redmine, per RFC 8414 §3.3. (#140)
- @Vitexus, exposed
current_useringet_mcp_server_infoand added the legacy-mode Redmine probe to/health(#139) - @Bricklou, requested publishing the Docker image to the GitHub Container Registry (#141)
- @timcomport, reported and diagnosed the OAuth discovery issuer mismatch on split-host deployments (#140)
2.1.0 - 2026-05-29
- Bump
starlette1.0.0 to 1.0.1 to fix PYSEC-2026-161 (malformedHostheader handling)
- OAuth mode now uses FastMCP v3 native auth. The hand-rolled
RedmineOAuthMiddleware(StarletteBaseHTTPMiddleware) is replaced byRemoteAuthProvider(token_verifier=IntrospectionTokenVerifier(...)). Token validation moves fromGET /users/current.jsonto Doorkeeper's RFC 7662 introspection endpoint (POST /oauth/introspect), exposing the token's actual scopes viaAccessToken.claims. This closes the medium-likelihoodcustom_routemiddleware-skip-list auth-bypass risk identified in the FastMCP v3 compatibility analysis.
-
Two new required env vars in OAuth mode:
REDMINE_INTROSPECT_CLIENT_IDandREDMINE_INTROSPECT_CLIENT_SECRET. Operators register a confidential OAuth client in Redmine and patch Doorkeeper'sallow_token_introspectionblock (stock Redmine ships withallow_token_introspection false). Seedocs/oauth-setup.mdStep 2 for the walkthrough. Server fails fast at startup if either env var is missing. -
Discovery path aliases dropped. Only the canonical paths remain:
GET /.well-known/oauth-protected-resource/mcp(RFC 9728 §3.1 suffix-scoped, mounted natively byRemoteAuthProvider)GET /.well-known/oauth-authorization-server/mcp(path-scoped RFC 8414 form, kept ascustom_routemirror of Redmine's Doorkeeper AS metadata)
These previously-served paths now return 404:
/.well-known/oauth-protected-resource(root),/mcp/.well-known/oauth-protected-resource(prefix),/.well-known/oauth-authorization-server(root),/mcp/.well-known/oauth-authorization-server(prefix). Clients should followWWW-Authenticate: Bearer resource_metadata="..."headers from 401 responses for RFC 9728 §5.3 compliant discovery. -
Upstream introspection failures now return 401 instead of 503. When Doorkeeper is unreachable, the previous behavior was
503 upstream_unavailable. FastMCP'sIntrospectionTokenVerifiertreats transport failures as auth failures, so clients see 401. Operators monitoring 503 spikes as an upstream-Redmine-down signal should switch to monitoring 401-rate or watch/health's new introspection probe (see Added).
/healthnow probes Doorkeeper's introspection endpoint in OAuth mode and surfaces the result as{"status": "ok"|"degraded", "checks": {"introspection": "ok"|"unreachable", "introspection_detail": "..."}}. Response remains HTTP 200 so container orchestrators continue treating the endpoint as a binary liveness probe; monitoring systems should inspect the JSONstatusfield. Results cached perHEALTH_INTROSPECTION_TTL_SECONDS(default 30s) to avoid hammering Doorkeeper on every health check.- Live OAuth integration test suite (
tests/test_oauth_integration.py) that exercises real Doorkeeper introspection against a sandbox Redmine. Runs only under--integrationwith sandbox creds, skips cleanly with a clear message when unconfigured. Seedocs/contributing.md"Live OAuth Integration Tests". - Structured warning logs on introspection upstream failures (
introspection_upstream_failure status_code=... url=...) so log-based alerting can distinguish real upstream issues from per-token 401s.
src/redmine_mcp_server/oauth_middleware.py(replaced by FastMCP native auth insrc/redmine_mcp_server/_auth.py).tests/test_oauth_middleware.py(replaced bytests/test_oauth_auth.pyandtests/test_oauth_discovery.py).
docs/oauth-setup.md: Step 2 rewritten with a Redmine-specific gotcha. The previously recommended approach of adding a separateconfig/initializers/doorkeeper.rbwith a freshDoorkeeper.configureblock silently wipes Redmine's entire Doorkeeper configuration (admin_authenticator, resource_owner_authenticator, grant_flows, scopes), because Doorkeeper'sconfigurerebuilds the Config wholesale rather than merging. The only safe override is editing the existingDoorkeeper.configureblock in Redmine'sconfig/initializers/30-redmine.rbin place. Also adds a note thatSetting.rest_api_enabledmust be true for Administration → Applications to be accessible.docs/troubleshooting.md: "all MCP requests return 401" diagnostic flow gains an entry for the standalone-initializer wipe symptom, which is otherwise indistinguishable from a misconfigured introspection client without checking the Doorkeeper warning log.docs/contributing.md: Reorganized the live OAuth integration test instructions around.env-based configuration (which the test module now honors).python tests/run_tests.py --integrationruns both general and OAuth integration suites. For OAuth-only filtering, the doc points atpython -m pytest tests/test_oauth_integration.pydirectly becauserun_tests.pydoes not forward-k-style filters.- Docs audit pass (no behavior changes):
README.md: Corrected stale tool counts — "44 MCP Tools" → "45 + 1 admin-gated"; "46 tools" header → "45 (+1)"; Issue Operations "(12 tools)" → "(13 tools)" to matchtools/issues.py.docs/contributing.md: Rewrote the architecture section for the v2.1 surface. Removed stale references tooauth_middleware.py, theContextVar-based token mechanism, and theGET /users/current.jsonvalidation path. Added_auth.py,oauth_scopes.py,_tool_error_middleware.pyand the missingtools/documents.py/tools/meta.pymodules to the inventory tables. Updated counts: 13 tool modules (was 11), 45 tools + 1 admin-gated (was 43).docs/tool-reference.md: Completed the read-only mode tool list in §157-176. The previous summary omittedcopy_issue,upload_file,delete_file,import_time_entries,update_checklist_item,manage_redmine_version, andmanage_document— all of which actually gate writes viaActionMode.WRITEper the per-tool sections.roadmap.md: Refreshed the Project Status block — v2.0.0 → v2.0.1 current, v2.1 noted as merged-to-develop, test count 1339 → 1365 (1285 unit + 80 integration). Updated "Last Updated" stamp.docs/oauth-setup.md,docs/troubleshooting.md,src/redmine_mcp_server/_env.py,src/redmine_mcp_server/_auth.py,tests/test_oauth_integration.py: Unified the operator-facing terminology around "confidential" rather thanprotected_resource?. Doorkeeper'sApplication#protected_resource?is an alias forconfidential?and isn't a separate UI toggle in Redmine's Admin → Applications form, so describing the requirement as "confidential" matches what operators actually click.
- Bump
uvicornfrom 0.47.0 to 0.48.0 (#137). Defaultsssl_cipherstoNone(uses OpenSSL defaults) and makesProxyHeadersMiddlewareignore duplicate forwarding headers. No behavioral impact for this server: TLS terminates at the proxy/Docker layer, and proxy-headers middleware is not enabled. - Bump
fastmcpfrom 3.2.4 to 3.3.1 (#128). Includes OAuth proxy hardening (silent-consent AS-in-the-middle guard, redirect URI dot-segment rejection, per-token response cache partitioning),OAuthProxy.update_scopes()public API, streamable-HTTP transport shutdown fix, and OTEL semconv compliance for list operations. The 3.3 packaging split (introducingfastmcp-slim) preserves all public import paths; 3.3.1 hotfixes a circular-import regression in 3.3.0. - Bump
uvicornfrom 0.46.0 to 0.47.0 (#127). Upstream adds anssl_context_factoryhook, eagerly imports the ASGI app in the parent process, and fixesfd=0handling under reload/workers. No behavioral impact for this server. - Bump
python-multipartfrom 0.0.27 to 0.0.29 (#126). 0.0.28 speeds up partial-boundary scanning and caps multipart boundaries at 256 bytes (hardening); 0.0.29 handles malformed RFC 2231 continuations inparse_options_header. Transitive viafastmcp-slim[server]; no API change. - Bump
codecov/codecov-actionfrom 6.0.0 to 6.0.1 (#125). Security patch (VULN-1652) preventing template injection inrun:steps. Pinned by commit SHA, verified against the upstreamv6.0.1signed tag from Codecov maintainer thomasrockhu-codecov. - Bump
blackfrom 26.3.1 to 26.5.1 (#129). Dev-only formatter. 26.5.0 adds Python 3.15 syntax support (PEP 798, PEP 810); 26.5.1 fixes stable-style edge cases around# fmt: skipand inline comments inside annotation subscripts. Published bypsf/black; no transitive dep changes. - Bump
astral-sh/setup-uvfrom 7 to 8.1.0 across all CI workflows (#102). v8.0.0 is a supply-chain hardening release that drops mutable major/minor tags (@v8,@v8.0) to prevent tj-actions-style attacks. SHA-pinning practice already in place is unchanged. v8.1.0 adds an opt-inno-projectinput (not used here). Verified SHA against the signed upstream tag from astral-sh maintainer eifinger.
tests/test_oauth_integration.pynow callspython-dotenv'sload_dotenv()at module-import time, soREDMINE_URL(and any other config) defined in.envis honored by the integration suite without having to re-export the var on the command line. Previously the test module reados.environdirectly at import time, which meant the suite skipped with "Missing: REDMINE_URL" unless the var was set in the shell, even when it was already in.envfor the running server.
- @aadnehovda, narrowed the RFC 8414 AS metadata path from root to
/.well-known/oauth-authorization-server/mcpso the MCP server no longer claims the root discovery slot when co-hosted with another app (#135).
2.0.1 - 2026-05-22
- Bump
urllib3from 2.6.3 to 2.7.0, patching CVE-2026-44431 and CVE-2026-44432; added explicit lower-bound constraint (urllib3>=2.7.0,<3) inpyproject.tomlto prevent silent regression to vulnerable versions (urllib3 is a transitive dep viarequests/python-redmine, so it had no direct floor before this).
- OAuth discovery documents now advertise
scopes_supportedon both/.well-known/oauth-protected-resourceand/.well-known/oauth-authorization-server. Without this field, MCP clients fell back to Doorkeeper's default scopes (view_project,search_project,view_members), so tools needing other permissions (list_redmine_issues,get_redmine_issue,list_time_entries,list_redmine_versions,summarize_project_status, and similar) returned 403 against OAuth-mediated requests. Read-only mode (REDMINE_MCP_READ_ONLY=true) hides write scopes from the advertised list. Resolves #130. - OAuth discovery documents are now also served at path-aliased locations (
/mcp/.well-known/...and/.well-known/.../mcp) so MCP clients that don't query the canonical root paths can still bootstrap the OAuth flow. - OAuth discovery now advertises Redmine's actual document permissions (
add_documents,edit_documents,delete_documents) instead of the inferred-but-nonexistentmanage_documents. The original #130 fix derived the scope from themanage_Xpattern used bymanage_wiki/manage_versions/manage_files, but Redmine's documents subsystem uses the granularadd_/edit_/delete_triad (same shape as issues). Redmine 6 / Doorkeeper enforcesenforce_configured_scopesagainstRedmine::AccessControl.permissions, so advertisingmanage_documentscaused/oauth/authorizeto reject the consent request withinvalid_scope: requested scope is invalid, unknown, or malformed, breaking the entire OAuth flow for any client that requested the full advertised scope set. Added a regression guardtests/test_oauth_scopes.py::test_advertised_scopes_are_real_redmine_permissionsthat checks every advertised scope against a committed snapshot ofRedmine::AccessControl.permissions(tests/fixtures/redmine_6_permissions.txt), so future pattern-based inference mistakes fail in unit tests rather than at a customer's consent screen.
- @timcomport, reported #130 with detailed diagnosis, reproduction steps, and analysis of prior PR #85's limitations.
- @aadnehovda, contributed the path-aliased discovery endpoints concept via
codex-dcr-compatfork, and identified the invalidmanage_documentsscope via live-test against Redmine 6 / Doorkeeper.
2.0.0 - 2026-05-16
delete_redmine_issue: new tool exposing irreversible issue deletion via Redmine'sDELETE /issues/{id}.json. The MCP surface previously hadcreate_redmine_issue/update_redmine_issue/copy_issue/get_redmine_issuebut no way to delete, so operators had to drop to the Redmine UI or python-redmine directly. Named to sit alongside the other*_redmine_issuelifecycle tools rather than under themanage_X(action=...)pattern, since there is only one verb. Mirrors thedelete_filesafety pattern: refuses unlessconfirm_delete=True, with a structuredimpactpreview (cascade counts for children, journals, attachments, time entries, inbound relations) in the refusal envelope. Subtask cascade requires a second opt-in (confirm_delete_with_children=True) so silent subtask destruction can't happen on a single misclick. Read-only mode blocks the call before any Redmine round-trip. Structured for the agent path (clear error codesCONFIRMATION_REQUIRED/CHILDREN_PRESENT/NOT_FOUND) and the operator path (explicit cascade preview). 14 tests intests/test_delete_redmine_issue.pycover the gate, the cascade preview, 404 handling at fetch and at delete time, invalid input, and read-only blocking (#120). Tool count: 45 → 46.get_mcp_server_info(no args, always callable): returns{server_version, read_only_mode, auth_mode, plugin_flags: {agile, checklists, products, crm, dmsf}}. Surfaced as an MCP tool so an LLM caller can detect deployment lag before relying on a recently-shipped fix -- compareserver_versionagainst the release / commit you expect. The response intentionally excludes credentials, internal hostnames, and file-system paths; only flags that change call shape are surfaced. Package version is sourced from installed metadata viaimportlib.metadata. Drift-guard: a regression test pins the exact set of returned keys plus the plugin-flag inventory so a future leak ofREDMINE_URL/REDMINE_API_KEY/PUBLIC_HOSTinto this response would fail CI loudly (#124). Tool count: 44 → 45.manage_document(gated byREDMINE_DMSF_ENABLED=true): single MCP tool covering DMSF (Document Management System for Files) plugin operations via anactionparameter. Requires theredmine_dmsfplugin (GPL v2) on the Redmine server.action="list": list documents in a project (or a specific DMSF folder viafolder_id); supportslimit(capped at Redmine's server-side 100/request)action="get": fetch a single document's metadata bydocument_idaction="create": upload a new document — two-step under the hood (POST /uploads.jsonto get a token, thenPOST /projects/{id}/dmsf/commit_files.jsonwith metadata). Acceptscontent_base64(raw bytes as base64),filename,title,description,comment,folder_id,version,custom_fields. Decoded payload capped at 50 MiB.action="update": update metadata fields (title,description,comment,custom_fields) by creating a new revision — DMSF is versioned and does not support in-place mutation. DMSF filenames are immutable; to replace file content,createa new revision with the same filename.- User-controlled fields (
filename,title,description,name, plus nestedauthor.name) wrapped in<insecure-content>boundary tags - Write actions respect
REDMINE_MCP_READ_ONLYand require_is_valid_project_id/_is_positive_intvalidation on path parameters - Whitelist filtering on
updaterejects unknown / immutable keys (e.g.,filename) with a clear error pointing at the create-new-revision workaround - 31 new unit tests covering feature-flag gating, all four actions, read-only mode, validation paths, base64 decoding errors, size-cap rejection, response shape variants (
{dmsf: [...]}vs bare list)
REDMINE_DMSF_ENABLEDenvironment variable (defaultfalse) documented in.env.example,.env.docker.example, and README_is_dmsf_enabled()helper in_env.py
- Renamed
.env.docker(previously tracked-but-gitignored placeholder, which trapped local edits as ongoing "modified" status and risked accidental commit of real credentials) to.env.docker.example, matching the.env.exampleconvention. Users now copy.env.docker.exampleto.env.docker, which stays untracked.deploy.shand the README quick-start were updated to copy from the new template name.
- List/search tools now return a flat
{"error": ...}envelope on failure (was sometimes[{"error": ...}]): before #117 several list-shaped tools (list_redmine_projects,list_project_issue_custom_fields,list_redmine_versions,list_project_members,list_time_entries,list_time_entry_activities,list_redmine_issues, and the validation-error paths inside several others) returned their error envelope wrapped in a single-element list to satisfy a strictList[Dict[str, Any]]return type. An agent that wanted to distinguish "failed call" from "empty result" had to checklen(result) == 1 and "error" in result[0]for some tools andisinstance(result, dict) and "error" in resultfor others. After #117 every list/search tool's return type is widened toUnion[List, Dict]and the failure path returns the flat{"error": ...}shape consistently. Backward-incompatible for callers that were keying off the array-wrapped form; the new shape matches the convention every other tool already follows. Drift-guard intests/test_list_error_envelope_consolidation.pyparametrizes across the list-tool surface and asserts a controlled exception inside each one produces a dict envelope, never an array (#117).
- Wiki attachments now expose
content_urlandauthor(cross-tool shape symmetry):manage_redmine_wiki_page(action="get", include_attachments=True)used to return each attachment withoutcontent_urlorauthor, whileget_redmine_issue(include_attachments=True)returned both. The asymmetry cost an agent a turn when handing off between the two read paths. Both code paths now route through a single new helper_attachment_to_dict()in_serialization.py, so the dict shape is identical:{id, filename, filesize, content_type, description, content_url, author, created_on}. Wrap policy from #109 is preserved consistently (filename verbatim, description wrapped, author via_named_ref), andcontent_urlis routed through_rewrite_to_public_url(#110) soREDMINE_PUBLIC_URLrewriting also applies to wiki attachments now. The shared helper means future attachment-serialization changes touch one site instead of three (issue, wiki, project file all use it -- DMSF still has its own raw-dict serializer because its payload shape differs, but the wrap policy lines up). A regression test intests/test_attachment_shape_symmetry.pypins the canonical key set and asserts the issue and wiki paths produce identical dicts (modulo the wrap-tag nonce ondescription) (#118).
create_redmine_issuenow accepts custom fields by name infields:fields={"Department": "Engineering"}is resolved tocustom_fields=[{"id": N, "value": "Engineering"}]automatically, bringing create to parity withupdate_redmine_issue. The shared resolution helper (_resolve_named_custom_fieldsin_custom_fields.py) is called from both paths: the update wrapper still does its issue-id-to-project-id lookup, the create wrapper takesproject_iddirectly. Ambiguous names raise; values are validated against the field'spossible_values. Closes the verification-thread asymmetry surfaced during #119 round-8; the validation-error hint is simplified now that the name-keyed shape works on both tools (#123).
- Padded
get_redmine_issueandcreate_redmine_issuedescriptions for tool_search recall: the first sentence of each docstring now includes synonym phrasings (get_redmine_issue: "fetch issue details", "view a ticket", "show a bug report", "get issue with comments";create_redmine_issue: "open a ticket", "file a bug", "submit a feature request", "log a support case", "report a task") so semantic-search-based tool discovery hits them on the call shapes an agent actually reaches for. Eval-driven: `tool_search` was missing `get_redmine_issue` on common phrasings until a multi-word query like "retrieve single issue with attachments journals" landed. The terse one-liner descriptions were doing too little work given how high-traffic these two tools are. Cross-references to neighbor tools (`list_redmine_issues`, `search_redmine_issues`, `copy_issue`, `update_redmine_issue`) added at the same time so an agent landing on the wrong one is steered to the right one without an extra round trip (#113). import_time_entries.entriesno longer accepts a JSON-string variant: tightened fromUnion[List[Dict], str]toList[Dict[str, Any]]. The string-then-parse path was an MCP oddity (most clients pass arrays natively), introduced a parallel error mode, and prevented the FastMCP boundary middleware from validating per-entry shape up front. Now the schema rejects strings/scalars at the boundary with the standardINVALID_ARGUMENTSenvelope from #108; direct Python callers passing a non-list hit a defense-in-depth guard with the same shape. Removes one line of unusedimport jsonfrom the module (#114).cleanup_attachment_filesis now operator-gated: the tool is no longer registered on the default MCP surface. It is admin/cron-style functionality (the background cleanup task in_cleanup.pyalready runs on theCLEANUP_INTERVAL_MINUTESschedule, so an LLM agent should almost never need it) and was creating discovery-noise an agent could waste a turn investigating. Operators driving cleanup through the MCP surface set the newREDMINE_MCP_EXPOSE_ADMIN_TOOLS=trueenv var to opt in; the function remains importable directly for internal callers (tests, scripts) regardless of the flag. The underlying background cleanup behavior is unchanged (#115).create_redmine_issue/update_redmine_issuevalidation-error envelope now carries a tailored recovery hint: when Redmine rejects with"<field> cannot be blank"/"is not included in the list"/"is invalid", the returned error dict is augmented withmissing_required_fields(parsed names) and ahintwhose body branches on whether the failing fields are Redmine standard fields, custom fields, or both. Standard fields (Subject, Priority, Tracker, Status, Assignee, etc.) get a hint pointing at the top-level /fields={"priority_id": N}shape with a pointer to the relevant discovery tool (list_redmine_issue_prioritiesetc.). Custom-looking fields get a hint covering the workingextra_fields={"custom_fields": [{"id": N, "value": "..."}]}shape on create, the name-keyed shape on update, and theREDMINE_AUTOFILL_REQUIRED_CUSTOM_FIELDSretry path. The custom-field branch also carries the #119 discovery-mismatch caveat:list_project_issue_custom_fieldsonly reflects the field-definitionis_requiredflag, while workflow rules, role-based field permissions, and tracker-bound required-field settings can still require fields at create/update time.list_project_issue_custom_fieldsdocstring updated with the same caveat plus the create/update recovery asymmetry (name-keyed shortcut on update only; tracked separately in #123) (#119).manage_documentaligned with the post-#109 / #111 / #112 conventions: the DMSF document-management tool that landed in PR #104 was prepared before the wrap-policy, bounded-limit, and action-enum changes shipped, so it carried three small drifts at merge time. Cleaned up: (1)actionis nowLiteral["list", "get", "create", "update"]so the JSON schema exposes the enum to strict MCP clients; (2)limitisAnnotated[int, Field(ge=1, le=100)]so out-of-range values are rejected at the FastMCP boundary byCleanValidationErrorMiddleware(#108); (3)_document_to_dictnow returnsfilename,name,title, andauthor.nameverbatim (onlydescriptionstays wrapped), matching the wrap policy from #109. The three drift-guard fixtures (tests/test_manage_action_schema.py,tests/test_limit_offset_schema.py,tests/test_wrap_policy.py) now covermanage_documentso future drift fails CI loudly (#122).list_redmine_issues.assigned_to_idandlist_time_entries.user_idnow reject arbitrary strings: both filter parameters used to be typedOptional[Union[int, str]]so they could accept the Redmine sentinel"me", but the widestrbranch also silently accepted any other string (assigned_to_id="garbage"returned[]-- the classic "agent reasons over the wrong count" footgun). Tightened toOptional[Union[int, Literal["me"]]]so the JSON schema renders the string branch as aconst: "me"enum, andCleanValidationErrorMiddleware(#108) rejects out-of-set values at the FastMCP boundary with the standardizedINVALID_ARGUMENTSenvelope listing both accepted shapes. Backward-incompatible only for callers that were passing strings other than"me"-- those calls were silently returning empty results, so the new error is strictly an improvement. Write-sideuser_id/assigned_to_idparameters (create/update/manage tools,manage_project_member,manage_issue_watcher,manage_time_entry) are unaffected -- they were already correctly typed asOptional[int](#116).- Stop wrapping structured-metadata fields in
<insecure-content>boundary tags: the wrapping was originally applied to every user-controllable string returned by the server (filenames, display names, short labels) on the theory that any text from Redmine could carry a prompt-injection payload. In practice, the wrapping created downstream friction (filenames had to be stripped before being used as paths/URLs/identifiers; assistant-rendered names showed boundary tags) without materially raising the bar against short-label injection. Following the eval recommendation in #109, the policy is now: free-text fields stay wrapped (description,notes, journal content, time-entrycomments, wiki pagetext, searchexcerpt, attachmentdescription, version/product/contactdescription/background); structured metadata is returned verbatim (filenames, all_named_refdisplay names -- author, version, project, tracker, status, role, etc. -- issue/checklist subjects, product/contact name fields, codes, identifiers). Affected return shapes:get_redmine_issue.attachments[*].filename,get_redmine_attachment.filename,list_files/upload_filefilename, contactfirst_name/last_name/middle_name/company/job_title/assigned_to.name, productname/code/project.name/category.name, issue categoryname, project member rolename, gantt issuesubjectand versionname. Backward-incompatible for any caller that explicitly stripped wrapper tags from these fields -- such callers can simplify. A pinning regression test intests/test_wrap_policy.pydocuments both halves of the policy so accidental drift fails CI (#109).
limitandoffsetnow carry explicit bounds in the JSON schema: every list/search tool'slimitandoffsetparameter is now annotated withAnnotated[int, Field(ge=..., le=...)](orOptional[int]forjournal_limit, whereNonesemantically means "no pagination") so strict MCP clients see the bounds, and out-of-range values are rejected at the FastMCP boundary byCleanValidationErrorMiddleware(#108) with the standardizedINVALID_ARGUMENTSenvelope. Previously, three tools (list_redmine_issues.limit,search_redmine_issues.limit,get_redmine_issue.journal_limit) rendered as theany-shapedOptional[int]with no constraints, and even theint-typed ones had no bounds despite their docstrings claiming caps. Affected tools and caps:get_gantt_chart.limit(1-500),get_redmine_issue.journal_limit/journal_offset(1-1000 / 0+),list_redmine_issues.limit/offset(1-1000 / 0+),list_redmine_users.limit/offset(1-100 / 0+),list_time_entries.limit/offset(1-100 / 0+),manage_contact.limit(1-100),manage_product.limit(1-100),search_entire_redmine.limit/offset(1-100 / 0+),search_redmine_issues.limit/offset(1-1000 / 0+). Existing defensive runtime clamping (e.g.min(limit, 1000)) is left in place for direct Python callers, where Pydantic validation does not run (#111).REDMINE_PUBLIC_URLenv var + serializer-layer URL rewrite: when Redmine is configured with an internal hostname (typical in Docker, e.g.http://redmine:3000), thecontent_urlit echoes back on attachments is unreachable from MCP clients on the host or the open internet, and a less-careful agent can waste a turn web-fetching it. A new_rewrite_to_public_url()helper rewrites any URL whose scheme+host+port matchesREDMINE_URL's origin to useREDMINE_PUBLIC_URL's origin instead, preserving path/query/fragment; foreign URLs (CDN-hosted assets, pre-rewritten values) are left untouched. Applied at the serializer layer (_file_to_dict,_attachments_to_list) so every code path returning attachment metadata benefits. The internal download path inget_redmine_attachmentdeliberately uses the raw URL — that call goes server-to-Redmine and must stay on the internal hostname. WhenREDMINE_PUBLIC_URLis unset (the default), the raw URL is returned and callers can fall back toget_redmine_attachmentfor a sandbox-safe download URL via the MCP server's proxy (#110).manage_*tools:actionis now a JSON-schema enum instead of a free-form string. The 10manage_Xtools (manage_contact,manage_issue_category,manage_issue_note,manage_issue_relation,manage_issue_watcher,manage_product,manage_project_member,manage_redmine_version,manage_redmine_wiki_page,manage_time_entry) declareactionas aLiteral[...]so strict MCP clients can validate the value, permissive clients get autocomplete, and invalid actions are rejected at the FastMCP boundary (with the standardizedINVALID_ARGUMENTSenvelope from #108) rather than reaching the dispatch decorator. A schema regression test pins each tool's allowed action set so drift between the dispatch spec and the public signature breaks loudly (#112).list_redmine_issues:status_idnow accepts Redmine's documented sentinel strings ("open","closed","*") in addition to a numeric status ID. Previously, passingstatus_id="open"(the first thing many LLM callers reach for, and the shape Redmine's REST API itself accepts) failed with a raw Pydantic int-parsing error; callers had to discover thefilters={"status_id": "open"}escape hatch. The widened type (int | Literal["open", "closed", "*"]) is reflected in the tool's JSON schema, so strict MCP clients also see the new accepted values (#107).- Pydantic-validation-error boundary middleware: a new FastMCP middleware (
CleanValidationErrorMiddleware) intercepts argument-validationpydantic.ValidationErrors raised before the tool body runs and returns the project's standard envelope ({"error", "hint", "code": "INVALID_ARGUMENTS", additional_errors?}) as both text andstructured_content. Previously, the raw Pydantic v2 dump (includingerrors.pydantic.devURLs) leaked through to the MCP caller, which was verbose and not actionable inside an LLM loop. The middleware honors FastMCP'sx-fastmcp-wrap-resultoutput-schema convention (tools returningUnion[List, Dict], etc.), so the envelope reaches strict clients intact instead of tripping a misleading "Output validation error: 'result' is a required property" on the client side. Union/Literal mismatches collapse all branch complaints into a singleerrormessage ("expected int or one of 'open','closed','*'"). Missing-required-argument errors name the parameter explicitly instead of echoing the whole args dict (#108).
get_redmine_attachment: when Redmine'sGET /attachments/{id}.jsonreturns 404, the tool now returns a structured envelope (code: ATTACHMENT_UNAVAILABLE,upstream_status: 404, plus ahint) instead of a bare"Attachment N not found."message. Redmine collapses three distinct conditions into the same 404 (genuine deletion, lack of view permission on the container, and orphan metadata whose underlying file is unreadable on the server's disk); callers that see the attachment viaget_redmine_issue(include_attachments=True)but get 404 here are usually hitting one of the last two. The new hint surfaces those causes and points to the embed path as the workaround so LLM callers can recover without a second wrong-tool round-trip (#106).search_redmine_issues: previously returnednullforsubject,status,priority,project,assigned_to,author,created_on, andupdated_onregardless of whatfieldsrequested, because Redmine's/search.jsonendpoint only populatesidand a description snippet. The tool now transparently hydrates each search hit via/issues.json(withstatus_id="*"so closed matches still hydrate), preserving search relevance order and falling back per-issue to the sparse search result for any id missing from the hydration response (e.g., deleted between calls). Hydration is skipped whenfieldsonly requestsidand/ordescription, so the lightweight one-call path is still available. Hydration failures are logged and degrade gracefully to the previous sparse behavior rather than raising. Large id sets are batched at 100 ids per/issues.jsoncall to stay within typical URL-length limits.
- Pin all GitHub Actions to immutable commit SHAs across all workflows to prevent supply chain attacks via tag hijacking (
actions/checkout,actions/setup-python,astral-sh/setup-uv,actions/github-script,codecov/codecov-action). Version tags are preserved as inline comments. - Bump
fastmcpfrom 3.2.0 to 3.2.4, patching three security issues: FileUpload now validates actual decoded base64 size instead of trusting client-reported size; proxy client no longer forwards inbound HTTP headers to unrelated remote servers; AuthKit auto-binds token audience to resource URL per RFC 8707, closing a token-reuse gap. - Bump
pytestfrom 9.0.2 to 9.0.3, patching CVE-2025-71176 (insecure temporary directory usage). - Bump
python-multipartfrom 0.0.26 to 0.0.27, patching CVE-2026-42561; added explicit lower-bound constraint to prevent silent regression to vulnerable versions.
- Bump
astral-sh/setup-uvfrom v4 to v7 (node24, faster version resolution for>=specifiers) - Bump
actions/github-scriptfrom v8 to v9 - Bump
codecov/codecov-actionfrom v5 to v6
get_redmine_attachment: unified attachment retrieval tool that works in both HTTP and stdio deployments- Downloads the attachment to local disk and returns an HTTP URI (
uri_type: "http") whenPUBLIC_HOST(orSERVER_HOST) resolves to an external hostname, or an absolute localfile_path(uri_type: "file") in stdio mode -- the model does not need to know which mode is active - Streaming download with configurable byte-cap abort (
ATTACHMENT_MAX_DOWNLOAD_BYTES, default 200 MB); partial files are deleted on abort - Atomic temp-file rename pattern (
{filename}.tmp-> final) consistent with existing file tools - All stored files go through the existing
AttachmentFileManagerexpiry and cleanup cycle filenamein the response is wrapped in<insecure-content>boundary tags (attacker-controlled)- Path traversal protection: filename sanitized to basename before writing to disk
- Host resolution follows the same fallback chain as the existing tool:
PUBLIC_HOST->SERVER_HOST->localhost; port resolved viaPUBLIC_PORT->SERVER_PORT->8000
- Downloads the attachment to local disk and returns an HTTP URI (
ATTACHMENT_MAX_DOWNLOAD_BYTESenvironment variable (default209715200, 200 MB): cap applied to allget_redmine_attachmentdownloads regardless of content type_get_int_env(var, default)helper in_env.pyfor numeric environment variables (all existing helpers are boolean_is_*functions)- 10 new unit tests covering HTTP mode, stdio mode,
SERVER_HOSTfallback, absolutefile_path, filename injection wrapping, byte-cap abort, metadata.json cleanup registration, path traversal sanitization, cap-abort leaving no partial files, and 404 error handling
get_redmine_attachment_download_url: removed in this major version. Useget_redmine_attachmentinstead, which works in both HTTP and stdio deployments.
- Consolidated 35 MCP tools into 9
manage_Xtools, reducing total tool count from 69 to 43:add_project_member,update_project_member,remove_project_member->manage_project_member(action=...)list_issue_categories,create_issue_category,update_issue_category,delete_issue_category->manage_issue_category(action=...)list_issue_relations,create_issue_relation,delete_issue_relation->manage_issue_relation(action=...)add_watcher,remove_watcher->manage_issue_watcher(action=...)edit_note,set_note_private->manage_issue_note(action=...)(get_private_noteskept standalone)create_time_entry,update_time_entry,log_time_for_user->manage_time_entry(action=...)get_redmine_wiki_page,create_redmine_wiki_page,update_redmine_wiki_page,delete_redmine_wiki_page,list_wiki_pages,rename_wiki_page->manage_redmine_wiki_page(action=...)list_products,get_product,add_product,edit_product->manage_product(action=...)(still gated byREDMINE_PRODUCTS_ENABLED=true)list_contacts,get_contact,create_contact,edit_contact,delete_contact,assign_contact_to_project,remove_contact_from_project->manage_contact(action=...)(still gated byREDMINE_CRM_ENABLED=true)mark_checklist_doneremoved: useupdate_checklist_item(is_done=True)directly
manage_time_entry(action="create", user_id=...)replaceslog_time_for_user- Verb normalization:
add_product/edit_productmap tomanage_product(action="create"|"update");create_contact/edit_contactmap tomanage_contact(action="create"|"update")to match the dominant CRUD pattern in the codebase - Response shape change: callers of
mark_checklist_donepreviously received{"is_done": bool}; the equivalentupdate_checklist_itemcall returns{"updated_fields": ["is_done"]} - Read-only mode: write actions within
manage_Xtools are blocked; read actions (list,get) remain available - Refactored
redmine_handler.py(6591 lines) into atools/package and focused private modules. The 43 MCP tools now live in 11 per-resource files undersrc/redmine_mcp_server/tools/, with shared helpers in flat_X.pymodules (_client.py,_errors.py,_validation.py,_serialization.py,_env.py,_custom_fields.py,_ssrf.py,_cleanup.py,_http_routes.py). Public MCP surface is unchanged (same 43 tools, parameters, return shapes, read-only behavior). Breaking for any consumer importing from internal paths: external code usingfrom redmine_mcp_server.redmine_handler import ...must migrate to the new module paths (e.g.,from redmine_mcp_server.tools.projects import manage_project_member,from redmine_mcp_server._validation import _is_positive_int). Theredmine_handlermodule is removed in v2.0.0. - Codified the
manage_X(action=...)pattern via a new@action_dispatchdecorator in_decorators.py. The 9manage_Xtools (plusmanage_redmine_version) now declare their action set as{action: ActionMode.READ|WRITE}and the decorator handles validation, read-only guards, and cleanup-task initialization. Futuremanage_Xtools should use this decorator for consistency.
- @mihajlovicjj: added
manage_documenttool for DMSF (Document Management System for Files) plugin support, with 31 new tests covering list/get/create/update across feature-flag gating, dispatch routing, two-step upload, byte-cap enforcement, and validation paths (#104)
1.3.0 - 2026-05-06
- Wiki management (2 new tools, no plugin required):
list_wiki_pages— list every wiki page in a project (titles, versions, parents, timestamps)rename_wiki_page— rename/move a wiki page with optional redirect (PUT /projects/{id}/wiki/{old}.jsonwithtitleparameter); detects silent permission failures by re-fetching the page after the rename
REDMINE_PRODUCTS_ENABLED=trueopt-in support for RedmineUP Products plugin:list_products— list products, optionally filtered by projectget_product— retrieve a single product by IDadd_product— create a new product (name + status_id required; supports description, price, currency, code, project_id, category_id, tag_list, custom_fields)edit_product— update product fields (whitelist filter on writable fields)
- Gantt chart (1 new tool, no plugin required):
get_gantt_chart— composite tool that aggregates issues + versions + relations into a structured Gantt response (start/due dates, progress, parent_id, precedes/blocks dependencies, milestones); supports date-range filters andinclude_closedflag
REDMINE_CRM_ENABLED=trueopt-in support for RedmineUP CRM plugin:list_contacts— list contacts with project/search/tags/assignee filtersget_contact— retrieve a single contact (with optionalinclude=notes,deals,contacts)edit_contact— update contact fields (whitelist filter)create_contact— create a new contact in a project (with first_name, last_name, company, email, phone, visibility, etc.)delete_contact— delete a contact entirelyassign_contact_to_project— add an existing contact to an additional projectremove_contact_from_project— remove a contact from a project (without deleting it)
manage_redmine_version: single MCP tool for full version lifecycle management (create, update, delete) via anactionparameteraction="create": create a version in a project with optionaldescription,status,due_date,sharing,wiki_page_title; defaults tostatus="open"andsharing="none"action="update": update any subset of fields on an existing version byversion_idaction="delete": delete a version byversion_id- Validates
actionandstatusvalues client-side with actionable error messages - Respects
REDMINE_MCP_READ_ONLYmode - 21 new unit tests covering all three actions, defaults enforcement, meta-param exclusion, read-only mode, and API error paths
REDMINE_CHECKLISTS_ENABLED=trueopt-in support for RedmineUP Checklists Pro plugin:get_checklist: retrieve all checklist items for an issue (id, subject, is_done, position, timestamps)update_checklist_item: update a checklist item's text, done state, or positionmark_checklist_done: convenience tool to toggle done/undone state of a checklist item
REDMINE_AGILE_ENABLED=trueopt-in support for RedmineUP Agile plugin:get_redmine_issueauto-includesstory_points,agile_sprint_id, andagile_position;update_redmine_issueacceptsstory_pointsin thefieldsdict- 14 new MCP tools for Issue Tracking:
- Copying and hierarchy:
copy_issue— duplicate an existing issue via Redmine's nativecopy_frommechanism, with optional field overrides and support for copying subtasks/attachmentslist_subtasks— list child issues of a given parent (subtasks are created via existingcreate_redmine_issuewithparent_issue_id)
- Issue relations (blocks, duplicates, precedes, etc.):
list_issue_relations— list all relations for an issuecreate_issue_relation— create a relation between two issues; validatesrelation_typeagainst Redmine's taxonomydelete_issue_relation— delete a relation by ID
- Watchers:
add_watcher— add a user to an issue's watcher list (Redmine 2.3.0+)remove_watcher— remove a user from an issue's watcher list
- Journal/note management:
edit_note— update an existing journal note's text and/orprivate_notesflag viaPUT /journals/{id}.jsonget_private_notes— retrieve only the private notes on an issue (requires "View private notes" permission)set_note_private— toggle the private/public state of an existing journal note
- Issue categories:
list_issue_categories— list all categories for a projectcreate_issue_category— create a new category (optionally with a default assignee)update_issue_category— rename a category or change its default assigneedelete_issue_category— delete a category with optionalreassign_to_idto move existing issues
- Copying and hierarchy:
- 55 new unit tests covering all new tools (read-only mode enforcement, success paths, error paths, helper conversions)
- 5 new MCP tools for Projects:
list_redmine_roles— list all roles defined in the Redmine instance; use beforeadd_project_member/update_project_memberto discover validrole_ids(role IDs vary between Redmine instances)get_project_modules— retrieve enabled modules for a project via?include=enabled_modulesadd_project_member— add a user or group to a project with assigned roles; validates that exactly one ofuser_idorgroup_idis providedupdate_project_member— update the roles of an existing membershipremove_project_member— remove a membership (inherited memberships from parent projects surface as a 422 validation error)
role_idsvalidation errors inadd_project_member/update_project_membernow hint atlist_redmine_rolesto prevent AI agents from hallucinating role IDs- 33 new unit tests for project tools covering modules retrieval (including dict-format fallback for older Redmine versions), role discovery, membership CRUD, validation errors, read-only mode enforcement, error paths, and error-message discoverability hints
- 2 new MCP tools for Time Tracking:
log_time_for_user— create a time entry on behalf of another user via theuser_idparameter onPOST /time_entries.json; requireslog_time_for_other_userspermission on the target projectimport_time_entries— bulk import multiple time entries via sequential API calls (Redmine has no native bulk endpoint); accepts a list of dicts or JSON array string, captures per-entry errors, and returns{total, succeeded, failed, created, errors}so partial imports still yield useful feedback; supportsstop_on_errorflag
- Add missing
REDMINE_MCP_READ_ONLYenforcement to existingcreate_time_entrytool - 23 new unit tests for time tracking tools covering success paths, per-entry validation (missing hours, negative hours, missing target), JSON string input, partial failure handling,
stop_on_error, field whitelisting, read-only mode, and Redmine-version permission quirks - 3 new MCP tools for Files:
list_files— list files uploaded to a project's Files section (core Redmine "Files" module, distinct from issue attachments and DMSF documents); returns filename, filesize, content type, description, download URL, author, optional version/releaseupload_file— upload a new file via Redmine's two-step upload (POST /uploads.jsonfor token, thenPOST /projects/{id}/files.json). Accepts eithersource_url(HTTP/HTTPS URL the server downloads from) orcontent_base64(raw bytes encoded as base64); the URL path enables chaining with other MCP tools that return download URLs (e.g., Google Drive MCP). Streaming download with 30s timeout, follows redirects, infers filename from URL path orContent-Dispositionheader. 50 MiB size capdelete_file— delete a project file viaDELETE /attachments/{id}.json
- 36 new unit tests for file tools covering base64 encoding/decoding, URL-based download (success, invalid schemes, HTTP errors, timeouts, empty body, Content-Disposition filename fallback), size limits, read-only mode, missing/conflicting content sources, and Redmine error paths
- 6 new MCP Discovery / Enumeration tools to help LLMs find valid IDs without guessing:
list_redmine_trackers— list all trackers (issue types like Bug, Feature, Support) for discovering validtracker_idvalueslist_redmine_issue_statuses— list all issue statuses with theiris_closedflag for discovering validstatus_idvalueslist_redmine_issue_priorities— list all priority levels viaenumeration.filter(resource="issue_priorities")list_redmine_users— filter/list users with optionalnameandgroup_idfilters (admin-only, limit clamped to 1-100)get_current_user— retrieve the authenticated user's profile viaGET /my/account.json(works for non-admins; useful when a user says "do X for me")list_redmine_queries— list all saved custom queries visible to the current user (read-only; Redmine's API does not support CRUD on queries)
- 20 new unit tests for discovery tools covering success paths, empty results, limit clamping, filter parameters, and permission-denied error paths
- SSRF protection for
upload_file(source_url=...): The server now resolves every URL hop and rejects non-public destinations (loopback, RFC1918, link-local including cloud metadata services like169.254.169.254, reserved, multicast). Redirects are followed manually with per-hop revalidation to defeat public-to-private 302 bypasses. Capped at 5 redirect hops. URLs with embedded credentials (http://user:pass@host) are refused up front to prevent credential leakage across redirects. SSRF error messages no longer include the resolved IP (logged at WARNING level instead) to avoid leaking internal network topology. Opt-in dev override:REDMINE_ALLOW_PRIVATE_FETCH_URLS=true. (Note: DNS rebinding between our check and httpx's connect is a theoretical residual risk; IP pinning was evaluated but broke TLS SNI for real CDNs.) delete_fileis now fail-closed on ambiguouscontainer_type: Previously, if Redmine (or an older python-redmine version) returnedNoneor an empty string forcontainer_type, the project-scope guard was skipped and the attachment was deleted. Now any non-"Project"value refuses the delete, with an explicitconfirm_delete_any_attachment=Trueflag for bypass.- Int-ID validators reject booleans and non-positive values: Python treats
True/Falseasint, sorole_ids=[True]would silently assign role ID 1 (often an elevated role). New_is_positive_inthelper is applied torole_ids,user_id, andgroup_idparameters inadd_project_member,update_project_member,add_watcher,remove_watcher, andlog_time_for_user. - Attacker-controlled display names wrapped: New
_named_ref()helper wrapsuser.name,author.name, andversion.namefields (all user-controlled) in<insecure-content>boundary tags. Applied via_named_refto_file_to_dictand_attachments_to_list(previously, attachment filenames/descriptions were only wrapped in the newlist_filesoutput but NOT inget_redmine_issue(include_attachments=True)-- now consistent across both). - Prompt-injection hardening:
filename,description, and time-entrycommentsreturned fromlist_files/upload_file/log_time_for_user/import_time_entriesare now wrapped in<insecure-content>boundary tags (matching existing issue/journal/description handling). Prevents attacker-controllable Redmine metadata from being treated as trusted instructions by downstream LLMs. - Error-message secret scrubbing:
_handle_redmine_errornow redacts API keys (?key=,X-Redmine-API-Key), Bearer tokens, HTTP basic-auth credentials, and the configuredREDMINE_API_KEYbefore returning errors to MCP callers. Logs still see the raw message. - Content-Disposition filename sanitization: URL-inferred and header-derived filenames are URL-decoded, stripped of path components (defeats
../../../etc/passwdtraversal on both POSIX and Windows), rejected if they contain null bytes or control characters, and capped at 255 chars. delete_filecontainer-type check: Since Redmine'sDELETE /attachments/{id}.jsonremoves any attachment by ID (including issue/wiki attachments),delete_filenow verifies the target is a project file before deleting. Callers can bypass withconfirm_delete_any_attachment=True.- Bump
cryptographyfrom 46.0.6 to 46.0.7, patching CVE-2026-39892 (out-of-bounds read via non-contiguous buffers)
_is_valid_project_idURL-path safety: Restricts string identifiers to Redmine's documented charset (^[a-z0-9][a-z0-9_-]{0,99}$), rejecting/,?,#,.., whitespace, and uppercase before they can be interpolated into URL paths in the new Wiki/Products/CRM tools.add_productstatus_idconstraint: Now rejects values other than1(Active) or2(Inactive) instead of forwarding arbitrary positive integers to the API.copy_issuedata-integrity bug: When bothcopy_subtasks=Falseandcopy_attachments=Falsewere passed, python-redmine'sinclude or (...)fallback silently copied both anyway. Now passes a non-empty sentinel so the fallback does not trigger.log_time_for_user/import_time_entrieshours validation: Now rejects NaN, Infinity, booleans (which Python treats asint), and non-numeric types before hitting the API.import_time_entriesbulk safeguards: Added a 500-entry batch cap (returns a clear error instead of pinning the event loop for minutes on a massive request). Yields the event loop between entries viaasyncio.sleep(0)so concurrent MCP requests are not starved. Split the create/serialize try blocks so a post-create serialization failure does not flip a successful create into a reported failure (which would tempt callers to retry and create duplicates).
get_gantt_chartdefaultinclude_closedis nowFalse. Passinclude_closed=Trueto retain prior behavior. Keeps response size and pagination cost low on long-lived projects.- List tools now return
Union[List, Dict]on error instead of[{"error": "..."}]. Affectslist_issue_relations,list_subtasks,list_issue_categories,list_files,list_redmine_roles,list_redmine_trackers,list_redmine_issue_statuses,list_redmine_issue_priorities,list_redmine_users,list_redmine_queries. Callers should checkisinstance(result, dict)or"error" in resultto distinguish failure from an empty list. Matches the pre-existing convention oflist_time_entries,list_redmine_issues, andsearch_redmine_issues. - List tools now cap results at 500 items (configurable via
_DEFAULT_LIST_RESULT_CAPconstant) via the new_iter_cappedhelper. Previously unbounded iteration could OOM on projects with tens of thousands of subtasks/relations/files. - Module-level constants consolidated:
_FILE_UPLOAD_MAX_SIZE_BYTES,_MAX_FILENAME_LEN,_IMPORT_TIME_ENTRIES_MAX_BATCH,_DEFAULT_LIST_RESULT_CAP,_DOWNLOAD_TIMEOUT, and_FILE_DOWNLOAD_MAX_REDIRECTSare all declared once at the top of the module instead of scattered across different sections. Top-levelimport httpxandfrom urllib.parse import unquote, urlparsehoisted out of function-local imports.
- Fix
pip-auditfailing on packages not published to PyPI by adding--no-emit-projecttouv exportindependency-audit.yml
- @mihajlovicjj — 30 new MCP tools, security hardening, and 82+ new tests (#89)
- @mihajlovicjj: 14 new MCP tools (Wiki, Products, Gantt, Contacts), security hardening, and 86 new tests (#98)
list_time_entry_activitiesnow accepts an optionalproject_idparameter to return project-specific activity IDs (fixes"Activity is not included in the list"errors when creating time entries for projects with custom activities)scripts/release.pynow supports--hotfixflag to finish ahotfix/*branch: bumps patch version, merges tomaster(tagged), merges back todevelop, and deletes the hotfix branchscripts/release.pymerge_back_to_developnow detects merge conflicts and exits with actionable instructions (resolve conflicts, stage files, commit, delete branch) instead of crashing with an unhandled error
1.2.0 - 2026-04-14
REDMINE_AGILE_ENABLED=trueopt-in support for RedmineUP Agile plugin:get_redmine_issueauto-includesstory_points,agile_sprint_id, andagile_position;update_redmine_issueacceptsstory_pointsin thefieldsdict
- Bump
fastmcpfrom 3.1.1 to 3.2.0, patching CVE-2025-64340 and CVE-2026-27124
- Fix
pip-auditfailing on packages not published to PyPI by adding--no-emit-projecttouv exportindependency-audit.yml
1.1.2 - 2026-04-08
- Fix
AttributeError: 'str' object has no attribute 'isoformat'crash when Redmine server returns date fields as pre-formatted strings instead of datetime objects (affects non-UTC timezone configurations and certain Redmine versions)
- Expand
test_safe_isoformat.pywith coverage for_issue_to_dict_selectiveandlist_redmine_projects-- the two paths omitted from the original PR
- @mihajlovicjj -- reported and fixed the
isoformatcrash on non-UTC Redmine configurations (#82)
1.1.1 - 2026-03-31
- Patch 14 CVEs across 7 transitive dependencies: pyjwt 2.12.1 (CVE-2026-32597), cryptography 46.0.6 (CVE-2026-26007, CVE-2026-34073), starlette 1.0.0 / fastapi 0.135.2 (CVE-2025-54121, CVE-2025-62727), urllib3 2.6.3 (CVE-2025-50181/82, CVE-2025-66418/71, CVE-2026-21441), requests 2.33.1 (CVE-2024-47081, CVE-2026-25645), python-multipart 0.0.22 (CVE-2026-24486), pygments 2.20.0 (CVE-2026-4539)
- Add
dependency-audit.ymlworkflow: lockfile integrity check (uv lock --check), CVE scanning viapip-audit, and PR step summary flagging lockfile changes for supply-chain review - Pin upper bounds on all runtime and dev dependencies in
pyproject.tomlto prevent unexpected major-version upgrades - Upgrade CI workflows to
actions/checkout@v6andactions/setup-python@v6 - Replace manual venv activation with
astral-sh/setup-uv@v4anduv sync --lockedfor reproducible installs - Use
uv runfor all tool invocations (flake8,black,pytest) instead of sourcing.venv
1.1.0 - 2026-03-21
- Version and auth mode are now logged at module import time, ensuring they appear in Docker deployments where the server is started via
uvicorn main:appdirectly (bypassingmain()) - Pass
log_config=Nonetouvicorn.run()to preserve the configured logging format in local deployments
- Migrated from
mcp[cli]>=1.25.0,<2tofastmcp>=3.0.0,<4(standalone FastMCP v3 package)
- Bump
uvicornfrom 0.40.0 to 0.42.0 - Bump
blackfrom 26.1.0 to 26.3.1 - Bump
python-dotenvfrom 1.2.1 to 1.2.2 - Updated import from
mcp.server.fastmcptofastmcp - Replaced
mcp.streamable_http_app()withmcp.http_app(stateless_http=True)(v3 API) - Removed
mcp.settings.stateless_httpruntime mutation (stateless_httpis now passed tohttp_app()) - Removed
host=parameter fromFastMCP()constructor (not a valid v3 parameter; DNS rebinding protection removed from FastMCP v3 entirely -- no behaviour change for Docker deployments) - Converted
list_redmine_issuesandsearch_redmine_issuesfrom**kwargsto explicit parameters (FastMCP v3 no longer supports**kwargstool functions); additional arbitrary filters still available viafilters={}/options={}dict parameters
1.0.0 - 2026-03-14
- New MCP Tool:
list_project_members- List members and groups of a Redmine project- Returns user/group info along with assigned roles
- Supports both numeric project IDs and string identifiers
- New MCP Tools: Time Tracking - Full time entry management
list_time_entries- List time entries with filtering by project, issue, user, and date rangecreate_time_entry- Log time against projects or issues with activity and date supportupdate_time_entry- Modify existing time entries (hours, comments, activity, date)list_time_entry_activities- Discover available activity types (Development, Design, etc.) for time entry creation- All tools support pagination and use
_get_redmine_client()for OAuth compatibility
- 50 new unit tests for project members and time tracking tools (
test_project_members.py,test_time_entries.py) - 26 new integration tests covering all 21 MCP tools with zero skips -- includes project members (4), time entries (7), custom fields (3), search issues (3), summarize project (3), global search (4), and cleanup (2)
- OAuth2 per-user authentication mode (
REDMINE_AUTH_MODE=oauth)- New
oauth_middleware.py: Starlette middleware that validatesAuthorization: Bearer <token>headers against Redmine's/users/current.jsonbefore forwarding MCP requests - Per-request token isolation via
contextvars.ContextVar-- safe under async concurrent load GET /.well-known/oauth-protected-resourceendpoint (RFC 8707) -- points MCP clients to the authorization serverGET /.well-known/oauth-authorization-serverendpoint (RFC 8414) -- advertises Redmine's Doorkeeper OAuth endpoints (/oauth/authorize,/oauth/token,/oauth/revoke) since Redmine does not serve this document itselfPOST /revokeendpoint (RFC 7009) -- proxies token revocation to Redmine's/oauth/revoke, enabling proper disconnect flow from MCP clients- PKCE (
S256) and bothclient_secret_post/client_secret_basictoken endpoint auth methods advertised - Requires Redmine 6.1+ (Doorkeeper OAuth2 support)
- New
REDMINE_AUTH_MODEenvironment variable -- selectslegacy(default) oroauthmode; legacy mode is unchanged so existing deployments require no changesREDMINE_MCP_BASE_URLenvironment variable -- public base URL of this server, used in OAuth discovery documents (only required in oauth mode)_get_redmine_client()factory function inredmine_handler.py-- creates a per-request Redmine client using OAuth token -> API key -> username/password priority; replaces the module-level shared client- 33 new unit tests for OAuth middleware, discovery endpoints, token revocation, and auth selection logic (
tests/test_oauth_middleware.py) - Prompt Injection Protection - User-controlled content from Redmine is now wrapped in unique boundary tags to prevent prompt injection attacks against LLM consumers
- New
wrap_insecure_content()function wraps non-empty strings in<insecure-content-{boundary}>tags with a random 16-character hex boundary per call - Applied to 6 helper functions:
_issue_to_dict(description),_issue_to_dict_selective(description),_journals_to_list(notes),_resource_to_dict(excerpt),_wiki_page_to_dict(text),_version_to_dict(description) - 22 new tests in
test_prompt_injection.py
- New
- Read-Only Mode - Block all write operations via
REDMINE_MCP_READ_ONLY=trueenvironment variable- Guards 5 write tools:
create_redmine_issue,update_redmine_issue,create_redmine_wiki_page,update_redmine_wiki_page,delete_redmine_wiki_page - Read tools (
get_redmine_issue,list_redmine_projects,list_redmine_issues, etc.) remain fully functional - Local operations (
cleanup_attachment_files) are not restricted - 15 new tests in
test_read_only_mode.py - Updated
.env.exampleand.env.dockerwithREDMINE_MCP_READ_ONLYvariable
- Guards 5 write tools:
- Journal Pagination on
get_redmine_issue- Newjournal_limitandjournal_offsetparameters for paginating through issue journals- When
journal_limitis set, response includesjournal_paginationmetadata (total,offset,limit,count,has_more) - Default behavior unchanged (returns all journals without pagination metadata)
- 9 new tests covering limit, offset, combined pagination, edge cases, and backward compatibility
- When
- Include Flags on
get_redmine_issue- Three new boolean parameters for fetching additional issue datainclude_watchers(default:false) - Returns watcher list withidandnameinclude_relations(default:false) - Returns issue relations withid,issue_id,issue_to_id,relation_typeinclude_children(default:false) - Returns child issues withid,subject,tracker- All flags default to
falsefor backward compatibility - Include parameters are passed to the Redmine API for server-side inclusion
- 11 new tests covering all flags, combinations, missing attributes, and structure validation
- Removed
list_my_redmine_issues- Deprecated since v0.11.0. Uselist_redmine_issues(assigned_to_id='me')instead.- All references in docstrings updated to point to
list_redmine_issues()
- All references in docstrings updated to point to
- Custom routes (well-known endpoints) not served at runtime --
mcp.run()created a fresh internal app discarding route registrations; switched touvicorn.run(app, ...)so the decorated app instance is always what serves requests REDMINE_URLKeyError at import time --oauth_middleware.pynow usesos.environ.get()instead ofos.environ[], so the server starts cleanly even ifREDMINE_URLis not set before import- Legacy client recreated on every tool call --
_get_redmine_client()now caches a singleton_legacy_clientin legacy mode instead of building a newRedmine()instance per request - OAuth routes exposed in legacy mode -- well-known endpoints and
/revokeare now only registered whenREDMINE_AUTH_MODE=oauth
main()now runs viauvicorn.run(app, ...)directly instead ofmcp.run(transport="streamable-http")to ensure custom route registrations are preserved
- Code Quality - Added
.flake8config for Black compatibility (E203 ignore)
- @mihajlovicjj -- OAuth2 per-user authentication,
/revokeendpoint, discovery endpoints, and 33 new tests (#71) - @mihajlovicjj -- Project members and time tracking tools with 50 new tests (#72)
0.12.1 - 2026-03-05
- 421 Misdirected Request in Docker/public deployments (#69)
- Pass
SERVER_HOSTto FastMCP so DNS rebinding protection is configured correctly - When host is
0.0.0.0(Docker/public), FastMCP skips auto-enabling DNS rebinding protection, avoiding 421 errors for connections via public IPs
- Pass
0.12.0 - 2026-02-19
- New MCP Tool:
list_project_issue_custom_fields- Discover issue custom fields for a Redmine project- Lists custom field metadata (
id,name,field_format,is_required,multiple,default_value) - Includes allowed values (
possible_values) and tracker bindings (trackers) - Optional
tracker_idfilter to show only fields applicable to a specific tracker - 7 unit tests covering serialization, filtering, validation, and error handling
- Lists custom field metadata (
- New MCP Tool:
list_redmine_versions- List versions/milestones for a Redmine project- Filter by
project_id(numeric or string identifier) - Optional
status_filterparameter (open, locked, closed) - Client-side filtering with input validation
- 18 unit tests covering helper, basic functionality, filtering, and error handling
- 6 integration tests for project ID, string identifier, structure, filtering, and error handling
- Filter by
fixed_version_idfilter documented forlist_redmine_issuestool- Claude Desktop MCP client configuration added to README with stdio transport via FastMCP proxy
get_redmine_issuenow supportsinclude_custom_fields(default:true) and can return serialized issuecustom_fields.update_redmine_issuenow supports updating custom fields by name (for example{"size": "S"}) by resolving project custom-field metadata.
- Required custom field handling for
create_redmine_issueandupdate_redmine_issue(#65)- Auto-retry on validation errors for missing required custom fields (e.g., "cannot be blank", "is not included in the list")
- Fills values from Redmine custom field
default_valueorREDMINE_REQUIRED_CUSTOM_FIELD_DEFAULTSenv var - Opt-in via
REDMINE_AUTOFILL_REQUIRED_CUSTOM_FIELDS=trueenvironment variable create_redmine_issuenow acceptsfieldsas a JSON object string for flexible custom field payloads- Added
REDMINE_REQUIRED_CUSTOM_FIELD_DEFAULTSenv var for specifying fallback values per field name - Updated
.env.exampleand.env.dockerwith new environment variables
create_redmine_issueextra_fieldsparameter -- Previously, passingextra_fieldsas a plain string would forward it directly to Redmine as an attribute. Now it is parsed as a JSON object (or dict) and merged into the issue payload. Callers who relied on the old behaviour of sending a rawextra_fieldsstring attribute should migrate tofieldsor provide a JSON object string instead.
- Dependency Updates
blackupgraded from 25.12.0 to 26.1.0
- Improved issue update validation for named custom fields with clear errors when values are not allowed for the target custom field.
- @sebastianelsner -- custom field discovery tool, required custom field handling, and custom field update support (#65, #66)
- Test Coverage - 44 new unit tests for custom field helper functions (
redmine_handler.pylines 474-640)- Covers
_is_true_env,_normalize_field_label,_parse_create_issue_fields,_extract_possible_values,_extract_missing_required_field_names,_load_required_custom_field_defaults,_is_missing_custom_field_value,_is_allowed_custom_field_value,_resolve_required_custom_field_value redmine_handler.pycoverage improved from 94% to 97% (with integration tests)- Overall coverage improved from 95% to 98%
- Covers
- Documentation - Updated README and tool-reference.md
- Tool count updated from 15 to 17
- Added
list_project_issue_custom_fieldsto Project Management category in README - Added full
list_project_issue_custom_fieldsdocumentation to tool-reference.md - Added
list_redmine_versionsto Project Management category in README - Added full tool documentation to tool-reference.md with parameters, examples, and usage guidance
- Documented
fixed_version_idparameter forlist_redmine_issues
0.11.0 - 2026-02-14
- New MCP Tool:
list_redmine_issues- General-purpose issue listing with flexible filtering (#64)- Filter by
project_id,status_id,tracker_id,assigned_to_id,priority_id,sort assigned_to_idsupports numeric user IDs or'me'for the authenticated userfieldsparameter for selective field returns to reduce token usage- Full pagination support with
limit,offset, andinclude_pagination_info - Supports string project identifiers (e.g.,
"my-project") in addition to numeric IDs
- Filter by
- Comprehensive Test Suite - 34 unit tests and 15 integration tests for the new tool
- Covers filters, pagination, field selection, combined filters, error handling, and MCP parameter unwrapping
- Integration tests verify real Redmine API behavior including sort order and field selection
list_my_redmine_issuesrefactored as a thin wrapper aroundlist_redmine_issues(assigned_to_id='me')- Full backward compatibility maintained
- All existing calls continue to work unchanged
list_my_redmine_issues- Will be removed in a future release- Use
list_redmine_issues(assigned_to_id='me')instead - Wrapper delegates all parameters to
list_redmine_issues
- Use
- Documentation - Updated README and tool-reference.md
- Tool count updated from 14 to 15
- Tool reference now the single source of truth for tool documentation
0.10.0 - 2026-01-11
- Wiki Page Editing - Three new MCP tools for full wiki page lifecycle management
create_redmine_wiki_page(project_id, wiki_page_title, text, comments)- Create new wiki pagesupdate_redmine_wiki_page(project_id, wiki_page_title, text, comments)- Update existing wiki pagesdelete_redmine_wiki_page(project_id, wiki_page_title)- Delete wiki pages- Includes change log comment support for create/update operations
- 17 new tests with comprehensive error handling coverage
- Centralized Error Handler - New
_handle_redmine_error()function for consistent, actionable error messages- Handles 12 error types: SSL, connection, timeout, auth, forbidden, server error, validation, version mismatch, protocol, not found, and more
- Error messages include specific error types, actionable guidance, and relevant context (URLs, resource IDs, environment variables)
- All 10 MCP tools updated to use centralized error handling
- 21 new tests added for comprehensive error handling coverage
- Logging Improvements - Replaced remaining
print()statements with properloggercalls throughout codebase
- Code Coverage Target - Increased Codecov target from 70% to 80%
- Test Coverage - Improved
redmine_handler.pycoverage from 93% to 99%- Added 29 new tests covering edge cases and error handling paths
- Total test count increased from 302 to 331
- Only 5 module initialization lines remain uncovered (import-time code)
- Documentation - Added MCP architecture lessons blog post to README resources section
0.9.1 - 2026-01-04
- BREAKING: Removed deprecated
download_redmine_attachment()function- Was deprecated in v0.4.0 with security advisory (CWE-22, CVSS 7.5)
- Use
get_redmine_attachment_download_url()instead for secure attachment downloads
- Dependency Updates
mcp[cli]pinned to >=1.25.0,<2 (from >=1.19.0) for latest stable v1.xuvicornupgraded from 0.38.0 to 0.40.0
- Test Coverage - Improved from 76% to 88% with comprehensive test suite enhancements
- CI/CD - Moved coverage upload from PR workflow to publish workflow
0.9.0 - 2025-12-21
- Global Search Tool -
search_entire_redmine(query, resources, limit, offset)for searching across issues and wiki pages- Supports resource type filtering (
issues,wiki_pages) - Server-side pagination with configurable limit (max 100) and offset
- Returns categorized results with count breakdown by type
- Requires Redmine 3.3.0+ for search API support
- Supports resource type filtering (
- Wiki Page Retrieval -
get_redmine_wiki_page(project_id, wiki_page_title, version, include_attachments)for retrieving wiki content- Supports both string and integer project identifiers
- Optional version parameter for retrieving specific page versions
- Optional attachment metadata inclusion
- Returns full page content with author and project info
- Version Logging - Server now logs version at startup
- Logging Improvements - Replaced
print()withloggingmodule for consistent log formatting
0.8.1 - 2025-12-11
- Test Coverage Badge - Added test coverage tracking via Codecov integration
- Unit Tests for AttachmentFileManager - Comprehensive test coverage for file management module
- Dependency Updates - Updated core and development dependencies to latest versions
python-dotenvupgraded from 1.1.0 to 1.2.1pytest-mockupgraded from 3.14.1 to 3.15.1pytest-covupgraded from 6.2.1 to 7.0.0pytestupgraded from 8.4.0 to 9.0.2uvicornupgraded from 0.34.2 to 0.38.0pytest-asyncioupgraded from 1.0.0 to 1.3.0blackupgraded from 25.9.0 to 25.12.0
- CI/CD Improvements - Updated GitHub Actions dependencies
actions/checkoutupgraded from 4 to 6actions/setup-pythonupgraded from 5 to 6actions/github-scriptupgraded from 7 to 8
- Issue Management Workflows - Added GitHub issue templates and automation
- Bug report and feature request issue templates
- Stale issue manager workflow for automatic issue cleanup
- Lock closed issues workflow
- Auto-close label removal workflow
- Dependabot Integration - Configured automated dependency updates for uv, GitHub Actions, and Docker
0.8.0 - 2025-12-08
- Removed private keys from repository - Addresses GitGuardian secret exposure alert
- Test SSL certificates now generated dynamically in CI/CD pipeline
- Added
generate-test-certs.shscript for local and CI certificate generation - Updated
.gitignoreto exclude all generated certificate files - Private keys no longer stored in version control
- SSL Certificate Configuration - Comprehensive SSL/TLS support for secure Redmine connections
- Self-Signed Certificates -
REDMINE_SSL_CERTenvironment variable for custom CA certificates- Support for
.pem,.crt,.cercertificate formats - Path validation with existence and file type checks
- Clear error messages for troubleshooting
- Support for
- Mutual TLS (mTLS) -
REDMINE_SSL_CLIENT_CERTenvironment variable for client certificate authentication- Support for separate certificate and key files (comma-separated format)
- Support for combined certificate files
- Compatibility with unencrypted private keys (Python requests requirement)
- SSL Verification Control -
REDMINE_SSL_VERIFYenvironment variable to enable/disable verification- Defaults to
truefor security (secure by default) - Warning logs when SSL verification is disabled
- Development/testing flexibility with explicit configuration
- Defaults to
- Integration Testing - 9 comprehensive integration tests with real SSL certificates
- Test certificate generation using OpenSSL
- Validation of all SSL configuration scenarios
- Certificate path resolution and error handling tests
- Self-Signed Certificates -
- Enhanced Redmine client initialization with SSL configuration support
- Updated environment variable parsing for SSL options
- Improved error handling for SSL certificate validation
- Security - Secure by default with SSL verification enabled
- Certificate path validation prevents configuration errors
- Clear warnings for insecure configurations (SSL disabled)
- Comprehensive logging for SSL setup and errors
- Flexibility - Support for various SSL deployment scenarios
- Self-signed certificates for internal infrastructure
- Mutual TLS for high-security environments
- Docker-compatible certificate mounting
- Documentation - Extensive updates across all documentation:
- README.md - New SSL Certificate Configuration section with examples
- Environment variables table updated with SSL options
- Collapsible sections for different SSL scenarios
- Link to troubleshooting guide for SSL issues
- docs/troubleshooting.md - Comprehensive SSL troubleshooting section
- 8 detailed troubleshooting scenarios with solutions
- OpenSSL command examples for certificate validation
- Docker deployment SSL configuration guide
- Troubleshooting checklist for common issues
- docs/tool-reference.md - New Security Best Practices section
- SSL/TLS configuration best practices
- Authentication security guidelines
- File handling security features
- Docker deployment security recommendations
- README.md - New SSL Certificate Configuration section with examples
- CI/CD - Added SSL certificate generation step to PyPI publish workflow
- Tests were failing in GitHub Actions due to missing test certificates
- Certificate generation now runs before tests in all CI workflows
0.7.1 - 2025-12-02
- Critical: Redmine client initialization failure when installed via pip (#40)
.envfile is now loaded from the current working directory first, then falls back to package directory- Previously, the server only looked for
.envrelative to the installed package location (site-packages), causing "Redmine client not initialized" errors for pip-installed users - Added helpful warning messages when
REDMINE_URLor authentication credentials are missing - Removed redundant
load_dotenv()call frommain.pyto avoid duplicate initialization
- Regression Tests - Added 8 new tests in
test_env_loading.pyto prevent future regressions:- Tests for
.envloading from current working directory - Tests for warning messages when configuration is missing
- Tests for CWD precedence over package directory
- Tests for
- No Breaking Changes - Existing configurations continue to work
- Recommended - Place your
.envfile in the directory where you run the server (current working directory) - Fallback - If no
.envfound in CWD, the package directory is checked as before
0.7.0 - 2025-11-29
- Search Optimization - Comprehensive enhancements to
search_redmine_issues()to prevent MCP token overflow- Pagination Support - Server-side pagination with
limit(default: 25, max: 1000) andoffsetparameters - Field Selection - Optional
fieldsparameter for selective field inclusion to reduce token usage - Native Search Filters - Support for Redmine Search API native filters:
scopeparameter (values: "all", "my_project", "subprojects")open_issuesparameter for filtering open issues only
- Pagination Metadata - Optional structured response with
include_pagination_infoparameter - Helper Function - Added
_issue_to_dict_selective()for efficient field filtering
- Pagination Support - Server-side pagination with
- Default Behavior -
search_redmine_issues()now returns max 25 issues by default (was unlimited)- Prevents MCP token overflow (25,000 token limit)
- Use
limitparameter to customize page size - Fully backward compatible for existing usage patterns
- Performance - Significant improvements for search operations:
- Memory efficient: Uses server-side pagination
- Token efficient: Default limit keeps responses under 2,000 tokens
- ~95% token reduction possible with minimal field selection
- ~87% faster response times for large result sets
- Documentation - Comprehensive updates:
- Updated
docs/tool-reference.mdwith detailed search parameters and examples - Added "When to Use" guidance (search vs list_my_redmine_issues)
- Documented Search API limitations and filtering capabilities
- Added performance tips and best practices
- Updated
0.6.0 - 2025-10-25
- Dependency Updates - Updated core dependencies to latest versions
fastapi[standard]upgraded from >=0.115.12 to >=0.120.0mcp[cli]upgraded from >=1.14.1 to >=1.19.0
- MCP Security Fix - Includes security patch from MCP v1.19.0 (CVE-2025-62518)
- FastAPI Enhancements - Benefits from latest bug fixes and improvements
- MCP Protocol Improvements - Enhanced capabilities from latest updates
0.5.2 - 2025-10-09
- Major README reorganization - Comprehensive cleanup for professional, user-focused documentation
- Created separate documentation guides:
docs/tool-reference.md- Complete tool documentation with examplesdocs/troubleshooting.md- Comprehensive troubleshooting guidedocs/contributing.md- Complete developer guide with setup, testing, and contribution guidelines
- Refactored MCP client configurations with collapsible
<details>sections - Removed development-focused content from README (moved to contributing guide)
- Created separate documentation guides:
0.5.1 - 2025-10-08
- Updated MCP client configurations - Comprehensive update to all MCP client setup instructions
- VS Code: Added native MCP support with CLI, Command Palette, and manual configuration methods
- Codex CLI: New section with CLI command and TOML configuration format
- Kiro: Updated to use mcp-client-http bridge for HTTP transport compatibility
- Generic clients: Expanded with both HTTP and command-based configuration formats
0.5.0 - 2025-09-25
- Python 3.10+ support - Expanded compatibility from Python 3.13+ to Python 3.10+
- CI/CD matrix testing across Python 3.10, 3.11, 3.12, and 3.13 versions
- BREAKING: Minimum Python requirement lowered from 3.13+ to 3.10+
- Updated project classifiers to include Python 3.10, 3.11, and 3.12
0.4.5 - 2025-09-24
- Enhanced PyPI installation documentation with step-by-step instructions
0.4.4 - 2025-09-23
- PyPI badges and links in README now point to correct package name
redmine-mcp-server
0.4.3 - 2025-09-23
- MCP Registry support with server.json configuration
0.4.2 - 2025-09-23
- PyPI package publishing support as
redmine-mcp-server - Console script entry point:
redmine-mcp-servercommand
0.4.1 - 2025-09-23
- GitHub Actions CI test failure in security validation tests
0.4.0 - 2025-09-22
get_redmine_attachment_download_url()- Secure replacement for attachment downloads- Comprehensive security validation test suite
download_redmine_attachment()- Useget_redmine_attachment_download_url()instead- SECURITY:
save_dirparameter vulnerable to path traversal (CWE-22, CVSS 7.5) - Will be removed in v0.5.0
- SECURITY:
- CRITICAL: Fixed path traversal vulnerability in attachment downloads (CVSS 7.5)
0.3.1 - 2025-09-21
- Integration test compatibility with new attachment download API format
0.3.0 - 2025-09-21
- Automatic file cleanup system with configurable intervals and expiry times
AUTO_CLEANUP_ENABLEDenvironment variable for enabling/disabling automatic cleanup (default: true)CLEANUP_INTERVAL_MINUTESenvironment variable for cleanup frequency (default: 10 minutes)ATTACHMENT_EXPIRES_MINUTESenvironment variable for default attachment expiry (default: 60 minutes)- Background cleanup task with lazy initialization via MCP tool calls
- BREAKING:
CLEANUP_INTERVAL_HOURSreplaced withCLEANUP_INTERVAL_MINUTESfor finer control
0.2.1 - 2025-09-20
- HTTP file serving endpoint (
/files/{file_id}) for downloaded attachments - Secure UUID-based file URLs with automatic expiry (24 hours default)
- New
file_manager.pymodule for attachment storage and cleanup management
- BREAKING:
download_redmine_attachmentnow returnsdownload_urlinstead offile_path
0.2.0 - 2025-09-20
- BREAKING: Migrated from FastAPI/SSE to FastMCP streamable HTTP transport
- BREAKING: MCP endpoint changed from
/sseto/mcp
0.1.6 - 2025-06-19
- New MCP tool
search_redmine_issuesfor querying issues by text.
0.1.5 - 2025-06-18
get_redmine_issuecan now return attachment metadata via a newinclude_attachmentsparameter.- New MCP tool
download_redmine_attachmentfor downloading attachments.
0.1.4 - 2025-05-28
- Deprecated
get_redmine_issue_commentstool. Useget_redmine_issuewithinclude_journals=Trueto retrieve comments.
get_redmine_issuenow includes issue journals by default.
0.1.3 - 2025-05-27
- New MCP tool
list_my_redmine_issuesfor retrieving issues assigned to the current user - New MCP tool
get_redmine_issue_commentsfor retrieving issue comments
0.1.2 - 2025-05-26
- Roadmap moved to its own document
- New MCP tools
create_redmine_issueandupdate_redmine_issuefor managing issues
0.1.1 - 2025-05-25
- Updated project documentation with correct repository URLs
- Updated LICENSE with proper copyright (2025 Kevin Tan and contributors)
0.1.0 - 2025-05-25
- Initial release of Redmine MCP Server
- MIT License for open source distribution
- Core MCP server implementation with FastAPI and SSE transport
- Two primary MCP tools:
get_redmine_issue(issue_id)- Retrieve detailed issue informationlist_redmine_projects()- List all accessible Redmine projects
- Comprehensive authentication support (username/password and API key)
- Docker containerization support