redmine-mcp-workflows 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- redmine_mcp_workflows-1.0.0/.gitignore +36 -0
- redmine_mcp_workflows-1.0.0/CHANGELOG.md +570 -0
- redmine_mcp_workflows-1.0.0/LICENSE +21 -0
- redmine_mcp_workflows-1.0.0/PKG-INFO +241 -0
- redmine_mcp_workflows-1.0.0/README.md +209 -0
- redmine_mcp_workflows-1.0.0/docs/tool-catalog.md +193 -0
- redmine_mcp_workflows-1.0.0/docs/workflow-validation.md +135 -0
- redmine_mcp_workflows-1.0.0/pyproject.toml +82 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/__init__.py +3 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/cache/__init__.py +1 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/cache/migrations.py +125 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/cache/schema_db.py +574 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/client.py +194 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/config.py +133 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/errors.py +277 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/schema/__init__.py +1 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/schema/custom_fields.py +94 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/schema/project.py +127 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/schema/tracker.py +151 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/schema/workflow.py +75 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/secrets.py +45 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/server.py +2631 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/__init__.py +1 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/attachments.py +371 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/bulk.py +414 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/comments.py +140 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/custom_fields.py +29 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/discovery.py +83 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/enumerations.py +57 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/files.py +125 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/forums.py +171 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/groups.py +189 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/issue_categories.py +131 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/issue_statuses.py +29 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/issues.py +862 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/memberships.py +140 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/news.py +162 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/passthrough.py +126 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/projects.py +173 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/queries.py +45 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/relations.py +185 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/roles.py +58 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/search.py +128 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/time_entries.py +262 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/users.py +107 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/versions.py +281 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/watchers.py +92 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/tools/wiki.py +216 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/validation/__init__.py +1 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/validation/fields.py +172 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/validation/permissions.py +59 -0
- redmine_mcp_workflows-1.0.0/src/redmine_mcp/validation/transitions.py +121 -0
- redmine_mcp_workflows-1.0.0/tests/__init__.py +0 -0
- redmine_mcp_workflows-1.0.0/tests/conftest.py +14 -0
- redmine_mcp_workflows-1.0.0/tests/fixtures/cassettes/.gitkeep +0 -0
- redmine_mcp_workflows-1.0.0/tests/test_cache.py +394 -0
- redmine_mcp_workflows-1.0.0/tests/test_client_auth.py +56 -0
- redmine_mcp_workflows-1.0.0/tests/test_config.py +150 -0
- redmine_mcp_workflows-1.0.0/tests/test_custom_fields_schema.py +184 -0
- redmine_mcp_workflows-1.0.0/tests/test_schema.py +422 -0
- redmine_mcp_workflows-1.0.0/tests/test_secrets.py +41 -0
- redmine_mcp_workflows-1.0.0/tests/test_server_passthrough.py +155 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/__init__.py +0 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/test_attachments.py +589 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/test_bulk.py +582 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/test_comments.py +241 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/test_forums.py +113 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/test_groups.py +123 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/test_issue_categories.py +156 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/test_issues.py +1279 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/test_memberships.py +117 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/test_news.py +139 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/test_passthrough.py +275 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/test_projects_crud.py +132 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/test_reference_data.py +191 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/test_relations.py +346 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/test_search.py +121 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/test_time_entries.py +281 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/test_users.py +110 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/test_versions.py +463 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/test_watchers.py +156 -0
- redmine_mcp_workflows-1.0.0/tests/test_tools/test_wiki.py +417 -0
- redmine_mcp_workflows-1.0.0/tests/test_validation.py +481 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
*.so
|
|
5
|
+
.Python
|
|
6
|
+
.venv/
|
|
7
|
+
venv/
|
|
8
|
+
ENV/
|
|
9
|
+
build/
|
|
10
|
+
dist/
|
|
11
|
+
*.egg-info/
|
|
12
|
+
*.egg
|
|
13
|
+
|
|
14
|
+
# Test
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.coverage
|
|
17
|
+
htmlcov/
|
|
18
|
+
.tox/
|
|
19
|
+
|
|
20
|
+
# Editor
|
|
21
|
+
.vscode/
|
|
22
|
+
.idea/
|
|
23
|
+
*.swp
|
|
24
|
+
*~
|
|
25
|
+
|
|
26
|
+
# Cache (local dev runs)
|
|
27
|
+
.cache/
|
|
28
|
+
*.db
|
|
29
|
+
*.db-journal
|
|
30
|
+
|
|
31
|
+
# vcrpy cassettes are checked in; only local test artifacts are ignored
|
|
32
|
+
.pytest_history
|
|
33
|
+
|
|
34
|
+
# Feature Factory / stargazer context (local-only)
|
|
35
|
+
.stargazer-*
|
|
36
|
+
.stargazer-context.json
|
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.7.0] — 2026-06-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`redmine_update_journal` .** Edit an existing journal entry's
|
|
14
|
+
notes in place via `PUT /journals/:id.json` (Redmine 5.0+). The API
|
|
15
|
+
user can edit their own notes; users with `edit_issue_notes` permission
|
|
16
|
+
can edit any note. Passing empty notes on a details-only journal
|
|
17
|
+
deletes it. Honors `REDMINE_MCP_READ_ONLY`.
|
|
18
|
+
|
|
19
|
+
## [0.6.0] — 2026-05-16
|
|
20
|
+
|
|
21
|
+
Held-field gate: non-ticket blockers for Redmine issues.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **`IssueHeld` structured error class.** New error type returned when
|
|
25
|
+
attempting to close a ticket that has a non-empty "Held" custom field.
|
|
26
|
+
Includes `issue_id`, `held_reason`, and optional `held_until` in the
|
|
27
|
+
structured payload. Error format:
|
|
28
|
+
`Cannot close #N: held — "reason" (held until YYYY-MM-DD)`.
|
|
29
|
+
- **`check_held_gate()` validator** in `validation/fields.py`. Inspects
|
|
30
|
+
an issue's `custom_fields` for a non-empty "Held" field. Returns
|
|
31
|
+
`IssueHeld` when the gate is active, `None` otherwise. Whitespace-only
|
|
32
|
+
and date-without-reason values are not considered holds.
|
|
33
|
+
- **Held-gate enforcement in `update_issue()`.** When a status transition
|
|
34
|
+
targets a closed status and the issue has a non-empty "Held" custom
|
|
35
|
+
field, `update_issue` returns `IssueHeld` before sending the PUT. This
|
|
36
|
+
covers `close_issue`, `bulk_close`, and `bulk_update_issues` since all
|
|
37
|
+
delegate to `update_issue`. Non-close transitions (e.g., New → In
|
|
38
|
+
Progress) are unaffected. The check runs before the workflow transition
|
|
39
|
+
check for efficiency (no user fetch needed).
|
|
40
|
+
|
|
41
|
+
## [0.5.0] — 2026-05-12
|
|
42
|
+
|
|
43
|
+
Dogfood-driven fixes from .
|
|
44
|
+
Feature work: Path A (OAuth2 bearer token),
|
|
45
|
+
(saved-query lookups), and (bulk-create).
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
- **`redmine_bulk_create_issues`.** Bulk-create from
|
|
49
|
+
per-spec dicts with subject idempotency. Pre-checks each subject within
|
|
50
|
+
its project via exact-match (post-filter on Redmine's substring-fuzzy
|
|
51
|
+
`subject` filter) and reports existing matches per `on_duplicate`:
|
|
52
|
+
`"skip"` (default) → `status="skipped"` + `duplicate_of`, `"fail"` →
|
|
53
|
+
`status="failed"` + `error="duplicate_subject"`, `"create_anyway"` →
|
|
54
|
+
bypass the lookup. Default 50ms pacing between POSTs (configurable;
|
|
55
|
+
empirically the floor for not tripping Redmine's per-issue rate cap
|
|
56
|
+
on small VMs). Returns `{results, summary}` with the full per-spec
|
|
57
|
+
outcome. ≤100 specs per call (matches MAX_BATCH_SIZE for the other
|
|
58
|
+
bulk tools). Replaces ~150 lines of direct-HTTP scaffolding in the
|
|
59
|
+
fleet's `ServerOps/scripts/redmine/create-*.py` exemplars with a
|
|
60
|
+
~30-line call.
|
|
61
|
+
- **`redmine_search_issues` accepts a `query_id` parameter
|
|
62
|
+
.** Forwards to Redmine's `?query_id=N` so callers
|
|
63
|
+
can invoke saved queries directly without dropping to the passthrough.
|
|
64
|
+
Layers with `status` / `project` / `query` / `limit` / `offset` per
|
|
65
|
+
Redmine's standard merge semantics. ``query_id=0`` (default) means
|
|
66
|
+
no saved query — current behavior unchanged.
|
|
67
|
+
- **OAuth2 bearer-token support (Path A).** New
|
|
68
|
+
`REDMINE_OAUTH_TOKEN` env var (and matching `REDMINE_OAUTH_TOKEN=...`
|
|
69
|
+
line in the secrets file) lets the wrapper authenticate via
|
|
70
|
+
Doorkeeper-issued access tokens (Redmine 6.1+) sent as
|
|
71
|
+
`Authorization: Bearer <token>`. When both `REDMINE_OAUTH_TOKEN` and
|
|
72
|
+
`REDMINE_API_KEY` are configured, the OAuth bearer wins — OAuth is the
|
|
73
|
+
explicit-opt-in path and shouldn't be shadowed by a stale API key in
|
|
74
|
+
`secrets.md`. New `Config.require_auth_headers()` returns the right
|
|
75
|
+
header dict; `Config.require_api_key()` retained for back-compat.
|
|
76
|
+
Path B (auth-code flow + local-callback PKCE) and Path C (device
|
|
77
|
+
grant — needs Redmine image rebuild) deferred to future tickets.
|
|
78
|
+
- **Silent status no-op detection on `redmine_update_issue` and
|
|
79
|
+
`redmine_close_issue`.** Previously, when Redmine accepted a PUT (2xx)
|
|
80
|
+
but didn't apply the requested `status_id` change — most commonly because
|
|
81
|
+
the `block_descendants_issues_closing` setting blocks parents with open
|
|
82
|
+
subtasks, or a custom workflow rule the cache hasn't yet observed — the
|
|
83
|
+
caller got a success-looking payload with the old status. Now the
|
|
84
|
+
post-PUT re-fetch includes `children` and the response is a structured
|
|
85
|
+
`status_change_silently_ignored` error with a hint that names the
|
|
86
|
+
blocking subtasks (if the target was a closed status and children exist)
|
|
87
|
+
or lists the likely causes generically. Returns `requested_status_id`,
|
|
88
|
+
`requested_status`, `actual_status_id`, `actual_status`, plus the issue
|
|
89
|
+
payload for inspection. Workflow observations are NOT recorded for
|
|
90
|
+
silent no-ops — only confirmed-moved transitions count.
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
- **`redmine_upload_attachment` verifies the attachment actually attached
|
|
94
|
+
.** Redmine's `PUT /issues/{id}.json` with `uploads:[...]`
|
|
95
|
+
silently drops the attachment under per-issue rate pressure (observed:
|
|
96
|
+
~1 PUT/sec cap), returning HTTP 200 regardless. The wrapper now does a
|
|
97
|
+
post-PUT `GET /issues/{id}.json?include=attachments` and checks that the
|
|
98
|
+
uploaded filename appears in `attachments[]`. If not, it retries the
|
|
99
|
+
PUT with the same token after 2s, then 5s, then surfaces a structured
|
|
100
|
+
`attachment_not_attached` error preserving the upload token so the
|
|
101
|
+
caller can recover manually. Backoffs tunable via
|
|
102
|
+
`ATTACHMENT_VERIFY_BACKOFFS` (tests monkeypatch to `(0, 0)`).
|
|
103
|
+
- **`redmine_list_projects` substring filter now searches across all
|
|
104
|
+
pages.** Previously the `query` filter only saw the first page returned
|
|
105
|
+
by `/projects.json` with the given `limit`/`offset` — matches that lived
|
|
106
|
+
later in the list silently dropped (caught dogfooding: `query="mcp"`
|
|
107
|
+
with default `limit=25` missed `kronos-mcp` even though it exists).
|
|
108
|
+
When `query` is set, list_projects now walks every page (capped at
|
|
109
|
+
1000 projects) before filtering, then re-slices by `limit`/`offset`.
|
|
110
|
+
`total_count` reflects the *filtered* total when filtering, so callers
|
|
111
|
+
can paginate over matches directly. Unfiltered behavior unchanged.
|
|
112
|
+
|
|
113
|
+
- **`redmine_create_issue` and `redmine_update_issue` expose more fields
|
|
114
|
+
.** New params on both:
|
|
115
|
+
- `due_date: str = ""` (ISO-8601 date)
|
|
116
|
+
- `start_date: str = ""` (ISO-8601 date)
|
|
117
|
+
- `done_ratio: int = -1` (sentinel for unchanged; `0` is explicit)
|
|
118
|
+
- `custom_fields: list | str = ""` (accepts native list or JSON string)
|
|
119
|
+
Plus `fixed_version_id` is now first-class on the `redmine_update_issue`
|
|
120
|
+
MCP wrapper (it was already supported internally). Previously these
|
|
121
|
+
fields were only reachable via `redmine_request`, which itself was
|
|
122
|
+
unusable . New `_normalize_custom_fields` helper handles the
|
|
123
|
+
dual list/string shape with a structured error for malformed input.
|
|
124
|
+
- **`redmine_request` accepts dict bodies and params.**
|
|
125
|
+
Some MCP transports auto-parse JSON-shaped string args into objects
|
|
126
|
+
before the tool sees them, which caused pydantic to reject the input
|
|
127
|
+
against the previous `body: str` schema with
|
|
128
|
+
`Input should be a valid string [type=string_type, input_value={...}]`.
|
|
129
|
+
Made the escape hatch genuinely an escape hatch: `body` and `params`
|
|
130
|
+
now accept either a JSON-encoded string (existing) or a dict (new),
|
|
131
|
+
with the wrapper normalizing to the internal dict form. Empty string
|
|
132
|
+
and empty dict both mean "no body / no params" (unchanged semantics).
|
|
133
|
+
|
|
134
|
+
### Notes
|
|
135
|
+
- 359 tests pass (was 309) — 50 new across the regression fixes + features.
|
|
136
|
+
|
|
137
|
+
## [0.4.0] — 2026-05-11
|
|
138
|
+
|
|
139
|
+
### Added
|
|
140
|
+
- **Difficulty custom-field support.** `redmine_create_issue` and
|
|
141
|
+
`redmine_update_issue` now accept a `difficulty` parameter for the
|
|
142
|
+
global `Difficulty` custom field. Values: `Unclassified` / `Easy` /
|
|
143
|
+
`Normal` / `Hard`. Engagement-mode signal (how much human oversight a
|
|
144
|
+
ticket needs), distinct from `Priority`.
|
|
145
|
+
- **Default-fill on create** — `redmine_create_issue` without a
|
|
146
|
+
`difficulty` arg (and without a `Difficulty` entry in `custom_fields`)
|
|
147
|
+
default-fills `Unclassified`, so auto-callers don't trip the
|
|
148
|
+
required-field validation. `redmine_update_issue` does **not**
|
|
149
|
+
default-fill (would silently overwrite user-set values).
|
|
150
|
+
- **Conflict resolution** — when both `difficulty=...` and a matching
|
|
151
|
+
`custom_fields` entry are passed, `difficulty` wins.
|
|
152
|
+
- **`redmine_describe_tracker`** output now includes a `custom_fields`
|
|
153
|
+
array listing the issue custom fields applicable to that tracker
|
|
154
|
+
(id, name, format_kind, is_required, default_value, possible_values,
|
|
155
|
+
applicable_tracker_ids, for_all_projects). Lazy-loads
|
|
156
|
+
`/custom_fields.json` on cache miss.
|
|
157
|
+
- New module `redmine_mcp.schema.custom_fields` with
|
|
158
|
+
`refresh_custom_fields()` + `get_custom_field_by_name()`.
|
|
159
|
+
- New schema-cache table `custom_fields` (migration v3) plus
|
|
160
|
+
`put_custom_field` / `get_custom_field` / `list_custom_fields` /
|
|
161
|
+
`get_custom_field_by_name` accessors on `SchemaCache`.
|
|
162
|
+
|
|
163
|
+
### Notes
|
|
164
|
+
- `/custom_fields.json` is admin-only on Redmine. If the configured API
|
|
165
|
+
key lacks admin scope, custom-field discovery silently returns empty —
|
|
166
|
+
`redmine_create_issue` / `redmine_update_issue` still work, but the
|
|
167
|
+
`difficulty=` convenience parameter cannot resolve the field id and
|
|
168
|
+
becomes a no-op.
|
|
169
|
+
- Redmine 6.x's REST list response omits the `is_for_all` key, so
|
|
170
|
+
`for_all_projects` is derived from absence of a `projects` array on
|
|
171
|
+
the field record.
|
|
172
|
+
|
|
173
|
+
Refs: serverops #2582, claudecode #2583.
|
|
174
|
+
|
|
175
|
+
### Security
|
|
176
|
+
- Bump `mcp` SDK pin to `>=1.23.0,<1.24.0` (running 1.23.3) to clear three high-severity advisories on the prior `<1.9.0` pin: MCP Python SDK missing DNS rebinding protection (fixed 1.23.0), FastMCP validation-error DoS (1.9.4), and Streamable HTTP Transport unhandled-exception DoS (1.10.0). Held on `<1.24.0` (vs. the fleet canary digitalocean-dns-mcp on `<2.0.0`) because this server is in active dogfood. All 279 tests pass on 1.23.3.
|
|
177
|
+
|
|
178
|
+
### Fixed
|
|
179
|
+
- `redmine_create_issue(project=...)` now accepts the project's display
|
|
180
|
+
name (e.g. `"Infra"`), not just the lowercase identifier slug
|
|
181
|
+
(`"infra"`). The natural round-trip pattern of reading
|
|
182
|
+
`redmine_get_issue(...).project.name` and feeding it back into
|
|
183
|
+
`redmine_create_issue` was failing with a generic `redmine_api_404`
|
|
184
|
+
because the slug lookup raised, the listing fallback never ran, and
|
|
185
|
+
there was no clue that "name vs slug" was the real problem
|
|
186
|
+
.
|
|
187
|
+
- `_resolve_project_id` now falls through slug → cache-by-name →
|
|
188
|
+
refreshed `/projects.json` listing-by-name before giving up.
|
|
189
|
+
Successful name resolutions warm the cache so subsequent calls are
|
|
190
|
+
cheap.
|
|
191
|
+
- `schema/project.describe_project` catches the upstream `RedmineAPIError(404)`
|
|
192
|
+
and returns a structured `project_not_found` dict instead of
|
|
193
|
+
bubbling the exception, so the resolver can fall through cleanly.
|
|
194
|
+
- `SchemaCache.get_project_by_name(name)` — case-insensitive search
|
|
195
|
+
across cached projects' `schema_json.name` field. The projects table
|
|
196
|
+
indexes on `identifier` only, so this is a small in-memory scan over
|
|
197
|
+
a small fleet of cached entries (correctness over performance per
|
|
198
|
+
the existing schema_db comment).
|
|
199
|
+
- When neither slug nor name resolves, the response is now the clean
|
|
200
|
+
structured `{"error": "project_not_found", "hint": "No project matches '<value>'.", ...}`
|
|
201
|
+
instead of a generic 404, so LLM callers get actionable specificity.
|
|
202
|
+
- 2 new tests in `tests/test_tools/test_issues.py`:
|
|
203
|
+
- `test_create_issue_resolves_project_by_display_name_via_cache` —
|
|
204
|
+
direct regression for #2568, exercises the cache-by-name path after
|
|
205
|
+
a 404 on the slug.
|
|
206
|
+
- `test_create_issue_resolves_project_by_name_via_list_refresh` —
|
|
207
|
+
exercises the list-refresh fallback when name isn't pre-cached, and
|
|
208
|
+
asserts the cache is warmed for next time.
|
|
209
|
+
- `test_create_issue_returns_project_not_found_when_unresolvable` was
|
|
210
|
+
rewritten to use the realistic 404-from-real-Redmine path (was
|
|
211
|
+
testing a fictional `200 OK` with `{"project": null}` body that real
|
|
212
|
+
Redmine never produces).
|
|
213
|
+
- 279 tests pass (+2 vs prior).
|
|
214
|
+
|
|
215
|
+
### Added
|
|
216
|
+
- v0.5 first feature: news + forum-board read tools (Redmine
|
|
217
|
+
#2390) — 2 new tools.
|
|
218
|
+
40 of the planned set live total.
|
|
219
|
+
- `tools/news.py` — `list_news(project=None, limit, offset)`. Empty
|
|
220
|
+
`project` hits `/news.json` (the global feed); a numeric id or slug
|
|
221
|
+
hits `/projects/{id}/news.json` (the project feed). Same tool name
|
|
222
|
+
covers both because callers always know which one they want and
|
|
223
|
+
splitting them adds no value.
|
|
224
|
+
- `tools/forums.py` — `list_messages(board_id, limit, offset)` →
|
|
225
|
+
`/boards/{board_id}/messages.json`. A 404 typically means the
|
|
226
|
+
boards module isn't enabled on the parent project; we surface the
|
|
227
|
+
structured error rather than masking. No `list_boards` companion —
|
|
228
|
+
`/projects/X/boards.json` is inconsistently enabled across Redmine
|
|
229
|
+
versions; `redmine_request` covers it for callers who need it.
|
|
230
|
+
- `server.py` — `redmine_list_news(project="", limit, offset)` and
|
|
231
|
+
`redmine_list_messages(board_id, limit, offset)`. Both read-only.
|
|
232
|
+
- 10 new unit tests in `tests/test_tools/test_news.py` (6) and
|
|
233
|
+
`tests/test_tools/test_forums.py` (4) covering global vs.
|
|
234
|
+
project-scoped paths, default + propagated pagination args, 404
|
|
235
|
+
surfacing, and defensive non-dict-response handling. 277 tests pass
|
|
236
|
+
total; ruff clean.
|
|
237
|
+
|
|
238
|
+
- v0.3 first feature: `redmine_request` generic-passthrough escape hatch
|
|
239
|
+
— 1 new
|
|
240
|
+
tool, opt-in. 38 of the planned set live total.
|
|
241
|
+
- `tools/passthrough.py` — sends arbitrary HTTP requests to any
|
|
242
|
+
Redmine REST endpoint with NO validation, NO workflow check, and
|
|
243
|
+
NO schema cache. Every response carries `validation_skipped: true`
|
|
244
|
+
plus a human-readable `warning` field so callers cannot accidentally
|
|
245
|
+
forget they bypassed the validation layer.
|
|
246
|
+
- **Gated behind `REDMINE_MCP_ENABLE_PASSTHROUGH=true`** — calls
|
|
247
|
+
return a structured `passthrough_disabled` error if the flag isn't
|
|
248
|
+
set. Default-off because the tool is in by-design tension with
|
|
249
|
+
redmine-mcp's "validate first" identity; users opt in when they
|
|
250
|
+
need an endpoint we don't yet wrap.
|
|
251
|
+
- `config.py` — new `enable_passthrough: bool` field plus parsing.
|
|
252
|
+
- `server.py` — new `redmine_request(method, path, body, params)`
|
|
253
|
+
tool. JSON-encoded body / params (parsed client-side). Honors
|
|
254
|
+
`REDMINE_MCP_READ_ONLY` for non-GET methods.
|
|
255
|
+
- 12 new unit tests in `tests/test_tools/test_passthrough.py` covering
|
|
256
|
+
every method (GET / POST / PUT / DELETE), method-case normalization,
|
|
257
|
+
empty-path / missing-leading-slash / unknown-method validation, the
|
|
258
|
+
universal `validation_skipped` flag, and error-envelope round-trip.
|
|
259
|
+
- 3 new tests in `tests/test_config.py` for the new env var (default
|
|
260
|
+
false, truthy / falsey value parsing). 267 tests pass total; ruff clean.
|
|
261
|
+
- Live smoke verified end-to-end:
|
|
262
|
+
read-only GET (with and without query params), POST → PUT → DELETE
|
|
263
|
+
round-trip on a transient passthrough-only ticket (#2440), error
|
|
264
|
+
envelopes for `OPTIONS` / missing-leading-slash / 404, plus gate-off
|
|
265
|
+
default and gate-on enable both confirmed at the server-tool layer.
|
|
266
|
+
|
|
267
|
+
### Added (continued)
|
|
268
|
+
- v0.2 seventh feature: versions / milestones CRUD (Redmine
|
|
269
|
+
#2382) — 6 new tools:
|
|
270
|
+
`list_versions`, `get_version`, `create_version`, `update_version`,
|
|
271
|
+
`delete_version`, `assign_issue_to_version`. 37 of the planned set
|
|
272
|
+
live total.
|
|
273
|
+
- `tools/versions.py` — full project-versions CRUD against
|
|
274
|
+
`/projects/{p}/versions.json` (list/create) and `/versions/{id}.json`
|
|
275
|
+
(get/update/delete). Status (`open`/`locked`/`closed`) and sharing
|
|
276
|
+
(`none`/`descendants`/`hierarchy`/`tree`/`system`) enums are
|
|
277
|
+
validated client-side so a typo fails fast with a hint instead of
|
|
278
|
+
a generic 422. `due_date` is shape-checked (`YYYY-MM-DD` regex);
|
|
279
|
+
Redmine still does the calendar validation.
|
|
280
|
+
- `tools/issues.py` — `update_issue` extended with
|
|
281
|
+
`fixed_version_id` parameter (passes through to the API; empty
|
|
282
|
+
string clears the assignment). 2 new tests cover the round-trip.
|
|
283
|
+
- `assign_issue_to_version` is a thin convenience wrapper over
|
|
284
|
+
`update_issue` that sets `fixed_version_id` (and accepts `0` as
|
|
285
|
+
the unassign sentinel).
|
|
286
|
+
- `server.py` — registered all 6 tools (5 mutating ones honor
|
|
287
|
+
`REDMINE_MCP_READ_ONLY`).
|
|
288
|
+
- 26 new unit tests (24 in `tests/test_tools/test_versions.py` covering
|
|
289
|
+
every tool's happy + validation + 404 paths, 2 in test_issues.py
|
|
290
|
+
for the new `fixed_version_id` round-trip). 252 tests pass total;
|
|
291
|
+
ruff clean.
|
|
292
|
+
- Live smoke verified end-to-end:
|
|
293
|
+
full create → get → list → assign-issue → unassign → update → delete
|
|
294
|
+
cycle on a transient `v02-smoke-*` version in the `claudecode` project
|
|
295
|
+
(assignment must precede the open→locked status flip — locked
|
|
296
|
+
versions reject new issue assignments).
|
|
297
|
+
|
|
298
|
+
### Added (continued)
|
|
299
|
+
- v0.2 sixth feature: bulk operations (Redmine
|
|
300
|
+
#2381) — 2 new tools:
|
|
301
|
+
`bulk_update_issues`, `bulk_close`. 31 of the planned set live total.
|
|
302
|
+
- `tools/bulk.py` — thin orchestrators over
|
|
303
|
+
`issues.update_issue` / `issues.close_issue` that validate input
|
|
304
|
+
once (non-empty list, ≤ MAX_BATCH_SIZE=100, at least one updatable
|
|
305
|
+
field on the update tool), iterate sequentially (Redmine has no
|
|
306
|
+
batch endpoint), and aggregate results into
|
|
307
|
+
`{total, succeeded, failed, skipped}`.
|
|
308
|
+
- `stop_on_error=True` halts the batch on first failure and lands
|
|
309
|
+
the unprocessed remainder in `skipped` so callers can retry just
|
|
310
|
+
those; the default (False) is best-effort.
|
|
311
|
+
- `server.py` — registered both tools (mutating, honor
|
|
312
|
+
`REDMINE_MCP_READ_ONLY`).
|
|
313
|
+
- 13 new unit tests in `tests/test_tools/test_bulk.py` covering
|
|
314
|
+
validation, success aggregation, mixed-failure aggregation,
|
|
315
|
+
`stop_on_error` short-circuit, and batch-size cap. 226 tests pass total.
|
|
316
|
+
- Live smoke verified: a single-element
|
|
317
|
+
`bulk_update_issues` posted a journal entry on test issue #2411,
|
|
318
|
+
and `bulk_close([])` returned `validation_failed` as expected.
|
|
319
|
+
|
|
320
|
+
### Added (continued)
|
|
321
|
+
- v0.2 fifth feature: issue relations (Redmine
|
|
322
|
+
#2380) — 4 new tools:
|
|
323
|
+
`list_relations`, `add_relation`, `remove_relation`,
|
|
324
|
+
`set_parent_issue`. 29 of the planned set live total.
|
|
325
|
+
- `tools/relations.py` — `list_relations` GETs
|
|
326
|
+
`/issues/{id}/relations.json`; `add_relation` POSTs to the same
|
|
327
|
+
path with the new relation; `remove_relation` DELETEs at the
|
|
328
|
+
top-level `/relations/{id}.json` URL (NOT nested under the
|
|
329
|
+
parent issue — easy trap); `set_parent_issue` lives here for
|
|
330
|
+
discoverability even though it's mechanically a PUT to
|
|
331
|
+
`/issues/{id}.json` with `parent_issue_id` (parent/child is a
|
|
332
|
+
field on the issue, not a relation record).
|
|
333
|
+
- Relation-type aliasing: callers often think in colloquial terms
|
|
334
|
+
(`related_to`, `blocked_by`, `duplicate_of`, `duplicated_by`,
|
|
335
|
+
`copy_of`) while Redmine's enum is the source-side form
|
|
336
|
+
(`relates`, `blocked`, `duplicated`, `duplicates`, `copied_from`).
|
|
337
|
+
A small alias map normalizes before posting; unknown types fail
|
|
338
|
+
fast client-side with a structured error listing the canonical
|
|
339
|
+
set.
|
|
340
|
+
- `set_parent_issue` accepts `parent_issue_id=0` as the unparent
|
|
341
|
+
sentinel (sends empty string to Redmine, which is the API's
|
|
342
|
+
"remove parent" form).
|
|
343
|
+
- `server.py` — registered all 4 tools (3 mutating ones honor
|
|
344
|
+
`REDMINE_MCP_READ_ONLY`).
|
|
345
|
+
- 15 new unit tests in `tests/test_tools/test_relations.py` covering
|
|
346
|
+
list / add (with type normalization + delay) / remove / set_parent
|
|
347
|
+
+ cross-project 422 propagation. 213 tests pass total.
|
|
348
|
+
- Live smoke verified: added a
|
|
349
|
+
`related_to` relation between test issue #2411 and #2412, listed
|
|
350
|
+
it, then removed it.
|
|
351
|
+
|
|
352
|
+
### Added (continued)
|
|
353
|
+
- v0.2 fourth feature: wiki page CRUD (Redmine
|
|
354
|
+
#2378) — 4 new tools:
|
|
355
|
+
`get_page`, `create_page`, `update_page`, `delete_page`. 25 of the
|
|
356
|
+
planned set live total.
|
|
357
|
+
- `tools/wiki.py` — Redmine's wiki API uses PUT for both
|
|
358
|
+
create-and-update (returning 201 vs 200 to distinguish). We split
|
|
359
|
+
them at the tool boundary by adding a GET pre-flight to
|
|
360
|
+
`create_page` that refuses to overwrite an existing page (returns
|
|
361
|
+
`wiki_page_already_exists` with the current version so the caller
|
|
362
|
+
can either back off or call `update_page`).
|
|
363
|
+
- `update_page` accepts an optional `version` parameter for
|
|
364
|
+
Redmine's optimistic-concurrency check; a stale version surfaces
|
|
365
|
+
as the underlying `redmine_api_409`. Both `create_page` and
|
|
366
|
+
`update_page` re-fetch after the PUT so the caller gets fresh
|
|
367
|
+
metadata (Redmine usually returns 204 on the write itself).
|
|
368
|
+
- Titles are URL-encoded with `urllib.parse.quote(safe="")` so
|
|
369
|
+
spaces, slashes, and unicode all survive the path. The project
|
|
370
|
+
segment passes through verbatim — Redmine accepts both numeric
|
|
371
|
+
ids and slugs in the wiki URL routing layer.
|
|
372
|
+
- `server.py` — registered all 4 tools (3 mutating ones honor
|
|
373
|
+
`REDMINE_MCP_READ_ONLY`).
|
|
374
|
+
- 21 new unit tests in `tests/test_tools/test_wiki.py` covering each
|
|
375
|
+
tool's happy path + 404 propagation + URL encoding + version-aware
|
|
376
|
+
fetch + already-exists rejection + empty-text validation. 198
|
|
377
|
+
tests pass total.
|
|
378
|
+
- Live smoke verified: full
|
|
379
|
+
create → get → re-create-rejected → update → delete → 404-confirmed
|
|
380
|
+
cycle on a transient page in the `claudecode` project.
|
|
381
|
+
|
|
382
|
+
### Added (continued)
|
|
383
|
+
- v0.2 third feature: watchers (Redmine
|
|
384
|
+
#2379) — 3 new tools:
|
|
385
|
+
`add_watcher`, `remove_watcher`, `list_watchers`. 21 of the planned
|
|
386
|
+
set live total.
|
|
387
|
+
- `tools/watchers.py` — `add_watcher` POSTs to
|
|
388
|
+
`/issues/{id}/watchers.json` (idempotent on the API side);
|
|
389
|
+
`remove_watcher` DELETEs `/issues/{id}/watchers/{user_id}.json`
|
|
390
|
+
(404 surfaces verbatim so callers can distinguish "not a watcher"
|
|
391
|
+
from "issue not found"); `list_watchers` reuses
|
|
392
|
+
`issues.get_issue(include="watchers")` and lifts the `watchers`
|
|
393
|
+
array to a top-level field (mirrors the `get_journals` pattern).
|
|
394
|
+
- `server.py` — registered the 3 tools (mutating ones honor
|
|
395
|
+
`REDMINE_MCP_READ_ONLY`).
|
|
396
|
+
- 7 new unit tests in `tests/test_tools/test_watchers.py` covering
|
|
397
|
+
each tool's happy + 404-propagation paths. 177 tests pass total;
|
|
398
|
+
ruff clean.
|
|
399
|
+
- Live smoke verified end-to-end:
|
|
400
|
+
add+list+remove cycle on test issue #2411.
|
|
401
|
+
|
|
402
|
+
### Added (continued)
|
|
403
|
+
- v0.2 second feature: time-entry CRUD (Redmine
|
|
404
|
+
#2377) — 4 new tools:
|
|
405
|
+
`create_time_entry`, `list_time_entries`, `update_time_entry`,
|
|
406
|
+
`delete_time_entry`. 18 of the 14+ tools live total.
|
|
407
|
+
- `tools/time_entries.py` — create accepts ``H:MM`` or decimal hours
|
|
408
|
+
formats (parsed to a single canonical float before round-trip);
|
|
409
|
+
activity names resolve through the cached
|
|
410
|
+
`time_entry_activities` enumeration; ``issue_id`` and
|
|
411
|
+
``project_id`` are mutually exclusive (issue wins). Update is
|
|
412
|
+
partial — only supplied fields are sent — and re-validates any
|
|
413
|
+
supplied hours. Delete returns `{"deleted": true}` and is
|
|
414
|
+
permanent (no soft-delete in Redmine).
|
|
415
|
+
- `validation/fields.py` — new `parse_hours()` and `validate_hours()`
|
|
416
|
+
helpers. Accepted forms: numeric, decimal string (`"2.5"`), or
|
|
417
|
+
`"H:MM"` (`"2:30"` → 2.5h). Negative values, `H >= 60`, malformed
|
|
418
|
+
strings, and booleans are rejected with structured errors.
|
|
419
|
+
- `errors.py` — new `TimeEntryHoursInvalid` payload class.
|
|
420
|
+
- `schema/tracker.py` — `refresh_global_enumerations` now also
|
|
421
|
+
fetches `time_entry_activities` (cached under that meta key, same
|
|
422
|
+
24h TTL as the other enumerations).
|
|
423
|
+
- `server.py` — registered the 4 tools; the 3 mutating ones honor
|
|
424
|
+
`REDMINE_MCP_READ_ONLY`.
|
|
425
|
+
- 25 new unit tests: 12 in `tests/test_validation.py` covering
|
|
426
|
+
`parse_hours` / `validate_hours` (valid + invalid + boundary forms),
|
|
427
|
+
13 in `tests/test_tools/test_time_entries.py` covering each tool's
|
|
428
|
+
happy + validation-failure + propagation paths. 170 tests pass total;
|
|
429
|
+
ruff clean.
|
|
430
|
+
- Live smoke verified end-to-end:
|
|
431
|
+
created time entry #1 against test issue #2411 with `H:MM` hours +
|
|
432
|
+
resolved-by-name activity, listed it, rejected an invalid-hours
|
|
433
|
+
update client-side, applied a valid decimal-hours update, then
|
|
434
|
+
deleted the entry.
|
|
435
|
+
|
|
436
|
+
### Added (continued)
|
|
437
|
+
- v0.2 first tool: `redmine_download_attachment` (Redmine
|
|
438
|
+
#2376).
|
|
439
|
+
- `tools/attachments.py` — `download_attachment` runs Redmine's
|
|
440
|
+
two-step download (GET `/attachments/{id}.json` for metadata, then
|
|
441
|
+
GET `/attachments/download/{id}/{filename}` for the bytes). Validates
|
|
442
|
+
downloaded byte count against the metadata's `filesize` BEFORE
|
|
443
|
+
writing — short reads are surfaced as `attachment_size_mismatch`
|
|
444
|
+
rather than silently saving a partial file.
|
|
445
|
+
- New `_is_save_path_allowed()` helper distinguishes target-not-yet-
|
|
446
|
+
existent from `_is_path_allowed`'s upload semantics: parent must
|
|
447
|
+
exist + be under the allowlist, target may not exist (or
|
|
448
|
+
`overwrite=True`). Symlinks are resolved on the parent before the
|
|
449
|
+
allowlist comparison so a symlinked decoy parent that points
|
|
450
|
+
outside the allowlist is rejected.
|
|
451
|
+
- `client.py` — `_request` extended with `binary: bool = False`; new
|
|
452
|
+
`get_binary()` method returns the raw response body (used for
|
|
453
|
+
attachment fetches that aren't JSON).
|
|
454
|
+
- `errors.py` — `AttachmentPathDenied` extended with two new reasons
|
|
455
|
+
(`parent_missing`, `exists_no_overwrite`) for the save-side checks.
|
|
456
|
+
- `server.py` — registered `redmine_download_attachment` (read-only;
|
|
457
|
+
no `write=True` flag because nothing in Redmine changes).
|
|
458
|
+
- 13 unit tests added to `tests/test_tools/test_attachments.py`
|
|
459
|
+
(6 cover `_is_save_path_allowed` directly including symlink-parent
|
|
460
|
+
rejection; 7 cover the tool itself including size-mismatch and
|
|
461
|
+
overwrite semantics). 136 tests pass total.
|
|
462
|
+
- Live smoke verified end-to-end:
|
|
463
|
+
downloaded attachment id 8 (the file uploaded during Phase 5 smoke
|
|
464
|
+
to test issue #2411), confirmed bytes match, confirmed the
|
|
465
|
+
no-overwrite and outside-allowlist rejections fire with the right
|
|
466
|
+
reason codes.
|
|
467
|
+
|
|
468
|
+
### Added (continued)
|
|
469
|
+
- Phase 5: Comments + attachments (3 new tools — all 13 v0.1 tools now live).
|
|
470
|
+
- `tools/comments.py` — `add_comment` (direct PUT with `notes` /
|
|
471
|
+
`private_notes`; rejects empty/whitespace notes client-side to avoid
|
|
472
|
+
no-op `updated_on` bumps) and `get_journals` (read-only, lifts
|
|
473
|
+
`journals` to a top-level field).
|
|
474
|
+
- `tools/attachments.py` — `upload_attachment` (path-restricted via
|
|
475
|
+
`Config.allowed_directories`, two-step Redmine flow: POST
|
|
476
|
+
`/uploads.json` then optional PUT `/issues/{id}.json` with the
|
|
477
|
+
uploads array). Path safety check resolves symlinks before the
|
|
478
|
+
allowlist comparison so a symlink under `/tmp` pointing at
|
|
479
|
+
`/etc/shadow` is rejected. On a successful upload + failed attach,
|
|
480
|
+
the response carries the upload token so the caller can retry the
|
|
481
|
+
attach without re-reading the bytes.
|
|
482
|
+
- `client.py` — `_request` extended with `content` and `headers`
|
|
483
|
+
parameters; new `post_binary()` method for the
|
|
484
|
+
`Content-Type: application/octet-stream` body shape Redmine's
|
|
485
|
+
`/uploads.json` requires.
|
|
486
|
+
- `errors.py` — added `AttachmentPathDenied` payload class
|
|
487
|
+
(distinguishes `outside_allowlist` vs `not_a_file` reasons).
|
|
488
|
+
- `server.py` — registered the 3 new MCP tools. The attachment tool
|
|
489
|
+
pulls `Config.allowed_directories` at registration time so the tool
|
|
490
|
+
function doesn't need a config singleton import.
|
|
491
|
+
- 21 unit tests across `tests/test_tools/test_comments.py` (9) and
|
|
492
|
+
`tests/test_tools/test_attachments.py` (12 — 5 cover the path-safety
|
|
493
|
+
helper directly including symlink-escape rejection). 123 tests pass
|
|
494
|
+
total; ruff clean.
|
|
495
|
+
|
|
496
|
+
### Added
|
|
497
|
+
- Phase 4: Issue lifecycle (5 new tools — 10 of the 13 v0.1 tools live).
|
|
498
|
+
- `tools/issues.py` — `get_issue`, `create_issue`, `update_issue`,
|
|
499
|
+
`close_issue`, `search_issues`. Each registered as an `@mcp.tool()`
|
|
500
|
+
handler in `server.py` via the existing `_wrap()` helper.
|
|
501
|
+
- `_wrap(..., write=True)` short-circuits write tools with
|
|
502
|
+
`ReadOnlyModeError` when `REDMINE_MCP_READ_ONLY=true`.
|
|
503
|
+
- `update_issue` is the marquee tool. On a status-changing call it:
|
|
504
|
+
pre-flights against the cache (`is_disallowed` short-circuits with a
|
|
505
|
+
`WorkflowTransitionDisallowed` payload populated by `allowed_next`),
|
|
506
|
+
sends the PUT, and records the outcome — `allowed` on success;
|
|
507
|
+
`disallowed` with the captured error text on a status-related 422.
|
|
508
|
+
The cache learns the workflow graph with each call.
|
|
509
|
+
- `close_issue` resolves the closed status from cached
|
|
510
|
+
`issue_statuses` (`is_closed=true`, falls back to id 5) and
|
|
511
|
+
repackages a workflow rejection with closure-specific framing.
|
|
512
|
+
- `search_issues` accepts substring query (Redmine `subject=~`) plus
|
|
513
|
+
optional project/status filters; passes through `"open"`/`"closed"`/
|
|
514
|
+
`"*"` special tokens case-sensitively so a named status like
|
|
515
|
+
`"Closed"` still resolves through the cache.
|
|
516
|
+
- Helpers in `issues.py` resolve project/tracker/priority/status
|
|
517
|
+
references against the cache, populating it on miss
|
|
518
|
+
(`describe_project`, `fetch_all_trackers`,
|
|
519
|
+
`refresh_global_enumerations`).
|
|
520
|
+
- 19 unit tests in `tests/test_tools/test_issues.py` covering each
|
|
521
|
+
tool's happy path plus 1–2 validation-failure paths (FakeClient
|
|
522
|
+
pattern, no network). 102 tests pass total.
|
|
523
|
+
|
|
524
|
+
### Changed
|
|
525
|
+
- `server.py` no longer uses `from __future__ import annotations`.
|
|
526
|
+
FastMCP's `add_tool` introspects parameter annotations via
|
|
527
|
+
`inspect.signature(...).parameters[...].annotation` and runs
|
|
528
|
+
`issubclass(...)` on them; under PEP 563 the annotations are strings,
|
|
529
|
+
which fails the check. Removing the future import lets every tool
|
|
530
|
+
(Phases 1–4) register correctly. Local-variable annotations still
|
|
531
|
+
use PEP 604 union syntax (Python 3.10+ native).
|
|
532
|
+
|
|
533
|
+
### Added
|
|
534
|
+
- Phase 3: Validation layer.
|
|
535
|
+
- `validation/transitions.py` — `is_disallowed`, `allowed_next`, `has_any_observation` (cache-backed reactive lookups; aggregates across role ids; includes role `0` for global-admin observations).
|
|
536
|
+
- `validation/fields.py` — `validate_required` (base required fields for create), `validate_custom_fields` (shape + optional id-allowlist).
|
|
537
|
+
- `validation/permissions.py` — `is_admin`, `role_names_for_project`, `require_role` (admin bypass + named-role allowlist).
|
|
538
|
+
- `errors.py` — added `WorkflowTransitionDisallowed`, `RequiredFieldMissing`, `CustomFieldUnknown`, `CustomFieldShapeError`, `RoleNotAuthorized` payload classes with structured `extra` fields and human-readable `hint`s.
|
|
539
|
+
- 24 unit tests (`test_validation.py`) covering all three validators + the error payload shapes.
|
|
540
|
+
- `pyproject.toml` per-file ruff override: N818 disabled in `errors.py` (the payload classes aren't Python exceptions).
|
|
541
|
+
|
|
542
|
+
### Added (continued)
|
|
543
|
+
- Phase 2: Schema fetchers + 4 new discovery tools.
|
|
544
|
+
- `schema/tracker.py` — `fetch_all_trackers`, `describe_tracker` (enriches with global statuses + priorities + observed workflow graph).
|
|
545
|
+
- `schema/project.py` — `describe_project` (cache-backed), `list_projects` (paginated, optional client-side substring filter).
|
|
546
|
+
- `schema/workflow.py` — `fetch_current_user`, `role_ids_for_project`, `record_outcome` (writes per-role observations).
|
|
547
|
+
- `tools/discovery.py` — added `describe_tracker`, `describe_project`, `list_projects`, `invalidate_cache`.
|
|
548
|
+
- `server.py` — registered the 4 new MCP tools (now 5 of the 13 v0.1 tools live).
|
|
549
|
+
- `cache/migrations.py` v2 — added `outcome`, `observation_count`, `last_error_text` columns to `workflow_transitions`; renamed `fetched_at` → `observed_at`.
|
|
550
|
+
- `cache/schema_db.py` — added `get_tracker_by_name`, `resolve_tracker`, `get_meta_json`/`put_meta_json` (TTL'd JSON blobs in cache_meta), `record_workflow_observation`, `get_workflow_observation`, `list_workflow_observations`.
|
|
551
|
+
|
|
552
|
+
### Changed
|
|
553
|
+
- Workflow validation pivoted from **pre-flight** to **reactive observation** because Redmine's `/workflows.json` returns 403 even for global admins. See `docs/workflow-validation.md` for the new design — the cache learns the workflow graph by recording the outcome of every status-change attempt, surfaces it via `describe_tracker(include_observations=true)`, and short-circuits known-disallowed transitions on subsequent calls.
|
|
554
|
+
|
|
555
|
+
### Added (continued)
|
|
556
|
+
- 24 new unit tests across `test_cache.py` (10 added: meta_json, workflow observations, resolve_tracker) and `test_schema.py` (12 new: tracker/project/workflow fetchers via a FakeClient stand-in). 59 tests passing total.
|
|
557
|
+
|
|
558
|
+
- Phase 1: Core plumbing.
|
|
559
|
+
- `secrets.py` — triple-pattern loader for `~/.claude/secrets.md` (`REDMINE_API_KEY=`, `redmine_api_key:`, `TROUBLE_API_KEY=`).
|
|
560
|
+
- `config.py` — `Config` dataclass with `from_env()` factory; parses all `REDMINE_*` and `REDMINE_MCP_*` env vars; `require_api_key()` helper.
|
|
561
|
+
- `errors.py` — `StructuredError`, `RedmineAPIError`, `ReadOnlyModeError`.
|
|
562
|
+
- `client.py` — async `RedmineClient` (httpx-backed, retry on 5xx, `paginate()` async generator).
|
|
563
|
+
- `cache/migrations.py` — schema-version-stamped DDL for 5 cache tables.
|
|
564
|
+
- `cache/schema_db.py` — `SchemaCache` with TTL enforcement, auth-fingerprint reconciliation, `invalidate(scope=...)`.
|
|
565
|
+
- `tools/discovery.py` — `redmine_list_trackers` (smoke-test entry).
|
|
566
|
+
- `server.py` — FastMCP entrypoint, registered with the user-scoped MCP config.
|
|
567
|
+
- 35 unit tests across `test_secrets.py`, `test_config.py`, `test_cache.py` (all passing).
|
|
568
|
+
- End-to-end smoke test: `claude mcp list` reports the server connected; `redmine_list_trackers` round-trips against `http://127.0.0.1:8281`.
|
|
569
|
+
- Phase 0: Project scaffolding, MIT license, README, pyproject.toml.
|
|
570
|
+
- Empty module skeleton for `src/redmine_mcp/` with package boundaries (cache, schema, validation, tools).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Léon "Avic" Simmons
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|