kitsu-mcp 0.3.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.
Files changed (35) hide show
  1. kitsu_mcp-0.3.0/LICENSE +21 -0
  2. kitsu_mcp-0.3.0/PKG-INFO +213 -0
  3. kitsu_mcp-0.3.0/README.md +193 -0
  4. kitsu_mcp-0.3.0/pyproject.toml +65 -0
  5. kitsu_mcp-0.3.0/pyproject.toml.orig +57 -0
  6. kitsu_mcp-0.3.0/src/kitsu_mcp/__init__.py +33 -0
  7. kitsu_mcp-0.3.0/src/kitsu_mcp/__main__.py +3 -0
  8. kitsu_mcp-0.3.0/src/kitsu_mcp/app.py +45 -0
  9. kitsu_mcp-0.3.0/src/kitsu_mcp/auth.py +177 -0
  10. kitsu_mcp-0.3.0/src/kitsu_mcp/client.py +244 -0
  11. kitsu_mcp-0.3.0/src/kitsu_mcp/config.py +77 -0
  12. kitsu_mcp-0.3.0/src/kitsu_mcp/custom_fields.py +155 -0
  13. kitsu_mcp-0.3.0/src/kitsu_mcp/formatters.py +67 -0
  14. kitsu_mcp-0.3.0/src/kitsu_mcp/http.py +78 -0
  15. kitsu_mcp-0.3.0/src/kitsu_mcp/links.py +79 -0
  16. kitsu_mcp-0.3.0/src/kitsu_mcp/lookups.py +240 -0
  17. kitsu_mcp-0.3.0/src/kitsu_mcp/models.py +534 -0
  18. kitsu_mcp-0.3.0/src/kitsu_mcp/plans.py +101 -0
  19. kitsu_mcp-0.3.0/src/kitsu_mcp/production.py +109 -0
  20. kitsu_mcp-0.3.0/src/kitsu_mcp/prompts.py +70 -0
  21. kitsu_mcp-0.3.0/src/kitsu_mcp/resources.py +36 -0
  22. kitsu_mcp-0.3.0/src/kitsu_mcp/server.py +9 -0
  23. kitsu_mcp-0.3.0/src/kitsu_mcp/tools/__init__.py +5 -0
  24. kitsu_mcp-0.3.0/src/kitsu_mcp/tools/assets.py +157 -0
  25. kitsu_mcp-0.3.0/src/kitsu_mcp/tools/custom_fields.py +34 -0
  26. kitsu_mcp-0.3.0/src/kitsu_mcp/tools/meta.py +149 -0
  27. kitsu_mcp-0.3.0/src/kitsu_mcp/tools/people.py +143 -0
  28. kitsu_mcp-0.3.0/src/kitsu_mcp/tools/previews.py +300 -0
  29. kitsu_mcp-0.3.0/src/kitsu_mcp/tools/projects.py +203 -0
  30. kitsu_mcp-0.3.0/src/kitsu_mcp/tools/shots.py +196 -0
  31. kitsu_mcp-0.3.0/src/kitsu_mcp/tools/tasks.py +191 -0
  32. kitsu_mcp-0.3.0/src/kitsu_mcp/tools/video.py +341 -0
  33. kitsu_mcp-0.3.0/src/kitsu_mcp/tools/write.py +472 -0
  34. kitsu_mcp-0.3.0/src/kitsu_mcp/validation.py +41 -0
  35. kitsu_mcp-0.3.0/src/kitsu_mcp/video.py +706 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Estienne Goigoux
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.
@@ -0,0 +1,213 @@
1
+ Metadata-Version: 2.4
2
+ Name: kitsu-mcp
3
+ Version: 0.3.0
4
+ Summary: MCP server for the Kitsu (CGWire) production tracker
5
+ Author: EstienneG
6
+ Author-email: EstienneG <enneitse@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Dist: fastmcp>=4.1
10
+ Requires-Dist: gazu>=1.3.1
11
+ Requires-Dist: pydantic>=2.9
12
+ Requires-Dist: pydantic-settings>=2.6
13
+ Requires-Dist: av>=14 ; extra == 'video'
14
+ Requires-Dist: pillow>=11 ; extra == 'video'
15
+ Requires-Python: >=3.12
16
+ Project-URL: Repository, https://github.com/EstienneG/kitsu-mcp
17
+ Project-URL: Issues, https://github.com/EstienneG/kitsu-mcp/issues
18
+ Provides-Extra: video
19
+ Description-Content-Type: text/markdown
20
+
21
+ # kitsu-mcp
22
+
23
+ MCP server (FastMCP + Gazu) for a Kitsu/Zou instance. Runs locally over stdio, or hosted over HTTP as a
24
+ Le Chat / AI Studio connector where each person logs in with their own Kitsu account. Read-only by default;
25
+ write tools are opt-in.
26
+
27
+ ## Install
28
+
29
+ You only need [uv](https://docs.astral.sh/uv/getting-started/installation/):
30
+
31
+ curl -LsSf https://astral.sh/uv/install.sh | sh
32
+
33
+ Then register the server with your agent, using your Kitsu login.
34
+
35
+ **Mistral Vibe**
36
+
37
+ vibe mcp add kitsu --transport stdio --command uvx --arg kitsu-mcp \
38
+ --env KITSU_HOST=https://your-studio.cg-wire.com/api \
39
+ --env KITSU_USER=you@example.com \
40
+ --env KITSU_PASSWORD='your-password'
41
+
42
+ **Claude Code**
43
+
44
+ claude mcp add kitsu --scope user \
45
+ -e KITSU_HOST=https://your-studio.cg-wire.com/api \
46
+ -e KITSU_USER=you@example.com \
47
+ -e KITSU_PASSWORD='your-password' \
48
+ -- uvx kitsu-mcp
49
+
50
+ The password is stored in plain text in the agent's config file (e.g. `~/.vibe/config.toml`).
51
+
52
+ ## Hosted connector (Le Chat, AI Studio)
53
+
54
+ Le Chat and AI Studio run in Mistral's cloud, so they need the server at a public HTTPS URL. One server serves
55
+ everyone: each person adds their own private connector with their Kitsu login, and every call runs as them,
56
+ with their Kitsu permissions.
57
+
58
+ **Run it.** Only `KITSU_HOST` is needed (no shared login). Deploy the image on a host that terminates HTTPS
59
+ (Fly.io, Render, a reverse proxy) and confirm it with `KITSU_BEHIND_TLS=true`:
60
+
61
+ docker run -p 8000:8000 -e KITSU_HOST=https://your-studio.cg-wire.com/api -e KITSU_BEHIND_TLS=true \
62
+ ghcr.io/estienneg/kitsu-mcp:latest
63
+
64
+ (or build it yourself with `docker build -t kitsu-mcp .`). Pin a version tag such as `:0.2.0` in production.
65
+
66
+ To try it locally without a proxy, run `KITSU_HOST=... kitsu-mcp --http`, which listens on 127.0.0.1:8000.
67
+
68
+ The supported entry point is `kitsu-mcp --http` (`--host` / `--port`, or `KITSU_HTTP_HOST` / `PORT`): it
69
+ applies the TLS check below, which `fastmcp run` would skip. The MCP endpoint is `/mcp`; `/health` answers
70
+ for health checks. The image runs as an unprivileged user.
71
+
72
+ **HTTPS is required.** Callers send their Kitsu password with every request, so the server refuses to listen
73
+ on anything but localhost unless the operator confirms TLS terminates in front of it (`--behind-tls` or
74
+ `KITSU_BEHIND_TLS=true`; deliberately not set in the image). It never logs credentials and keeps them only in
75
+ memory, under keys derived with a per-process secret. Only logins Kitsu accepted are kept, for an hour of
76
+ inactivity. Repeated rejected logins for one account are slowed down (a Kitsu outage does not count), while a
77
+ login Kitsu accepted in the last day is always let through, so guessing at someone's email cannot lock them
78
+ out. Spraying many emails from one address is not throttled; fine at studio scale, otherwise rate-limit at the
79
+ proxy. Listing the tools works without a login
80
+ (it is metadata only); every tool call needs one.
81
+
82
+ **Each user** (AI Studio → Connectors → Add, or Le Chat → Connectors → Custom MCP connector):
83
+ - URL: `https://<your-server>/mcp`, visibility **Private**
84
+ - Authentication: **Basic**, with their Kitsu email and password. Where only a bearer token can be entered,
85
+ use `email:password` as the token.
86
+
87
+ Differences from stdio: `download_previews` and `download_video` are hidden (they would write to the server's
88
+ disk, not the user's); give people the `url` (Kitsu page) or `download_url` (original upload) instead, which
89
+ open in their own Kitsu browser session. The
90
+ video tools cache movies per user, and `KITSU_VIDEO_CACHE_MB` caps all users' movies together. Write tools
91
+ follow `KITSU_ALLOW_WRITE` as over stdio. Kitsu accounts with two-factor authentication cannot log in this
92
+ way.
93
+
94
+ ## Configuration
95
+
96
+ | Variable | Default | |
97
+ |---|---|---|
98
+ | `KITSU_HOST` | required | Zou API URL, ending in `/api` |
99
+ | `KITSU_USER` / `KITSU_PASSWORD` | required over stdio | Kitsu login (over HTTP each caller sends their own) |
100
+ | `KITSU_ALLOW_WRITE` | `false` | Expose the write tools below |
101
+ | `KITSU_CACHE_TTL` | `300` | Seconds to cache projects, task types, statuses, people, departments, custom fields |
102
+ | `KITSU_TIMEOUT` | `30` | Per-request timeout (seconds) |
103
+ | `KITSU_MAX_RETRIES` | `3` | Attempts for reads failing with a server or network error |
104
+ | `KITSU_VIDEO_CACHE_DIR` | system temp | Where the video tools cache movies (`kitsu-mcp-videos/`) |
105
+ | `KITSU_VIDEO_CACHE_MB` | `1024` | Movie cache size; least recently used movies are deleted past it |
106
+ | `KITSU_VIDEO_MAX_MB` | `500` | Largest movie the video tools download |
107
+ | `KITSU_OMITTED_STATUSES` | `omit,na,n/a,omitted,cancelled,canceled` | Statuses meaning "dropped": left out of progress and open work |
108
+ | `KITSU_DONE_STATUSES` | (none) | Statuses meaning "finished", whatever Kitsu flags (e.g. `cbb` for "could be better") |
109
+ | `KITSU_ON_HOLD_STATUSES` | `stand by,standby,pause,paused,blocked,on hold` | Open but paused: never overdue |
110
+ | `KITSU_REVIEW_STATUSES` | (none) | Extra statuses meaning "waiting for review/approval" |
111
+ | `KITSU_RETAKE_STATUSES` | (none) | Extra statuses meaning "sent back for changes" |
112
+ | `KITSU_DRY_RUN_LOG` | (none) | JSONL file each write-tool dry run is appended to |
113
+
114
+ Variables can also come from a `.env` file in the working directory.
115
+
116
+ ### Status roles
117
+
118
+ Kitsu's flags on a status (done, retake, feedback request, in progress) do not always say what a studio
119
+ means: an OMIT status is often flagged done but means dropped, and a studio may flag a "could be better" (CBB)
120
+ status as retake while meaning done-but-improvable (set `KITSU_DONE_STATUSES=cbb` for that). So every status gets exactly one **role** — `omitted`, `done`, `on_hold`, `retake`,
121
+ `waiting_review`, `wip` or `todo` — and every count and filter uses it (progress, overdue, waiting for
122
+ review, workload, search). A status listed in one of the `KITSU_*_STATUSES` settings above (names or short
123
+ names, case-insensitive, checked in the order shown) takes that role; any other status gets it from
124
+ Kitsu's flags, in the order done > retake > feedback request > in progress > to do.
125
+
126
+ `list_task_statuses` shows each status's role and where it comes from, and reports configured names that
127
+ match no status. `project_overview` reports which statuses it counted as omitted and as
128
+ done-but-improvable.
129
+
130
+ ## Development
131
+
132
+ cp .env.example .env # fill in KITSU_USER / KITSU_PASSWORD
133
+ uv run kitsu-mcp # reads .env from the current directory
134
+ uv run pytest # tests run against an in-memory fake Kitsu, no credentials needed
135
+ uv run ruff check . && uv run ruff format --check . && uv run pyright
136
+
137
+ Layout (`src/kitsu_mcp/`): `config.py` settings, `client.py` HTTP client (login, re-login, retries,
138
+ error mapping), `lookups.py` cached reference data and name resolution, `models.py` output schemas,
139
+ `formatters.py` payload → model conversion, `production.py` project-wide task views, `plans.py` write plans
140
+ (dry run), `custom_fields.py` custom field values and filters, `tools/` one module per domain,
141
+ `resources.py`, `prompts.py`, `auth.py` per-caller credentials, `http.py` HTTP mode (hidden local tools,
142
+ `/health`, TLS guard), `links.py` Kitsu web links.
143
+
144
+ Release: bump `version` in `pyproject.toml`, merge, then tag it (`git tag v0.2.0 && git push origin v0.2.0`).
145
+ The Release workflow checks the tag matches the version, runs the checks, then publishes the package to
146
+ PyPI and the Docker image to `ghcr.io/estienneg/kitsu-mcp` (tags `0.2.0`, `0.2` and `latest`).
147
+ PyPI uses trusted publishing, so no token is stored: the `kitsu-mcp` project on PyPI has this repository
148
+ as its publisher (workflow `release.yml`, environment `pypi`).
149
+
150
+ ## Tools
151
+
152
+ Project, task type, department, status and person arguments take names (case-insensitive) or IDs.
153
+ `departments` filters (`search_tasks`, `workload`, `project_overview`) keep tasks whose task type belongs to
154
+ one of the departments, so the three tools agree; people's own departments are not used (`list_people` shows
155
+ them).
156
+
157
+ | Area | Tools |
158
+ |---|---|
159
+ | Meta | `whoami`, `kitsu_get` (raw read of any Zou `data/` route), `refresh_cache` |
160
+ | Projects | `list_projects`, `list_task_types`, `list_task_statuses`, `list_asset_types`, `project_overview` |
161
+ | Assets | `list_assets` (type, name and per-task-type status filters; `detail="compact"` or `"full"`), `get_asset`, `get_casting` (either way) |
162
+ | Shots | `list_episodes`, `list_sequences`, `list_shots`, `get_shot`, `get_casting` |
163
+ | Custom fields | `list_custom_fields` (a project's custom columns: type, choices, departments); values show up by display name in `get_asset` / `get_shot` (and `detail="full"` lists), and `list_assets` / `list_shots` filter on them with `custom_field_filters` |
164
+ | Tasks | `search_tasks` (type, department, status, assignee, due dates, overdue, waiting feedback, group by assignee, ...), `get_task`, `task_history`, `my_tasks` |
165
+ | People | `list_people`, `get_person_tasks`, `workload` |
166
+ | Previews | `recent_previews` (latest uploads), `get_preview` (image or metadata, by entity or preview file), `list_task_previews`, `download_previews`, `list_playlists`, `get_playlist` |
167
+ | Video (`[video]` extra) | `get_video_info`, `get_video_contact_sheet`, `get_video_frames`, `compare_revisions`, `review_video`, `download_video` |
168
+ | Write (opt-in) | `add_comment`, `set_task_status`, `assign_task`, `update_task_schedule`, `log_time`, `create_playlist`, `add_to_playlist` (shots/assets with their latest preview) |
169
+
170
+ **Links:** detail results carry a `url` to the Kitsu page (asset, shot, task, preview in the player, playlist),
171
+ and preview metadata a `download_url` to the original upload. They open in a browser signed in to Kitsu, so
172
+ movies are watched and downloaded there rather than passed through the server.
173
+
174
+ **Dry run.** Every write tool takes `dry_run=True`: it runs the same lookups and checks with reads only and
175
+ returns the plan instead of writing: `would` (the action in one line), `requests` (method, path and payload,
176
+ exactly what the real call sends), `before`/`after` for the fields that change (status, assignees, dates,
177
+ estimate, time logged), and `warnings` (e.g. Kitsu may refuse a status your role cannot set). If a check fails
178
+ (archived status, inactive person, start date after due date, over 24 h logged in a day), `verdict` is
179
+ `would_fail` with the `reason`, and the real call refuses with the same reason. Invalid arguments (bad IDs,
180
+ unknown names) are errors either way. Agents are told to dry-run first and show the user the plan.
181
+
182
+ **Resources:** `kitsu://projects`, `kitsu://task-types`, `kitsu://task-statuses`,
183
+ `kitsu://projects/{project}/overview`, `kitsu://tasks/{task_id}`.
184
+
185
+ **Prompts:** `project_status_report`, `review_queue`, `my_day`, `person_summary`, `review_shot`.
186
+
187
+ ## Video
188
+
189
+ The video tools look inside movie previews. They need the optional `video` extra (PyAV, which
190
+ bundles ffmpeg, and Pillow), so no system ffmpeg is required:
191
+
192
+ uvx --from 'kitsu-mcp[video]' kitsu-mcp # or: pip install 'kitsu-mcp[video]'
193
+
194
+ Use that as the command when registering the server (e.g. `-- uvx --from 'kitsu-mcp[video]' kitsu-mcp`
195
+ for Claude Code). Without the extra the tools are still listed but answer with an error saying how
196
+ to install it (`download_video` works either way). `get_preview` says when a
197
+ preview is a video.
198
+
199
+ - Movies are fetched at Kitsu's low-resolution playback quality by default (`quality="original"` for
200
+ the uploaded file) and cached per preview file and quality (see the `KITSU_VIDEO_*` settings).
201
+ - Tools take a `preview_file_id`, or an `entity_id` (shot or asset) to use its current preview.
202
+ - Frames are numbered from 1, like Kitsu's player. Frame arguments accept frame numbers (`37`),
203
+ timecodes (`"00:00:01:12"`, HH:MM:SS:FF), times (`"1.5s"`) and percentages (`"50%"`).
204
+ `review_video` finds the frames a task's comments point at: Kitsu's `v3 00:00:01:12 (37)` tags,
205
+ timecodes, "frame 37", "2.5s", and frames with drawn annotations.
206
+ - Limits: at most 16 frames per call, grids at most 1568 px on a side, single frames at most
207
+ 1280 px. The agent only sees sampled still frames: no motion in between and no audio. Every image
208
+ costs context, so start with `get_video_contact_sheet` and zoom in with `get_video_frames`.
209
+
210
+ ## License
211
+
212
+ MIT: see [LICENSE](LICENSE). Free to use, modify and distribute, including commercially, provided the
213
+ copyright notice is kept; no warranty.
@@ -0,0 +1,193 @@
1
+ # kitsu-mcp
2
+
3
+ MCP server (FastMCP + Gazu) for a Kitsu/Zou instance. Runs locally over stdio, or hosted over HTTP as a
4
+ Le Chat / AI Studio connector where each person logs in with their own Kitsu account. Read-only by default;
5
+ write tools are opt-in.
6
+
7
+ ## Install
8
+
9
+ You only need [uv](https://docs.astral.sh/uv/getting-started/installation/):
10
+
11
+ curl -LsSf https://astral.sh/uv/install.sh | sh
12
+
13
+ Then register the server with your agent, using your Kitsu login.
14
+
15
+ **Mistral Vibe**
16
+
17
+ vibe mcp add kitsu --transport stdio --command uvx --arg kitsu-mcp \
18
+ --env KITSU_HOST=https://your-studio.cg-wire.com/api \
19
+ --env KITSU_USER=you@example.com \
20
+ --env KITSU_PASSWORD='your-password'
21
+
22
+ **Claude Code**
23
+
24
+ claude mcp add kitsu --scope user \
25
+ -e KITSU_HOST=https://your-studio.cg-wire.com/api \
26
+ -e KITSU_USER=you@example.com \
27
+ -e KITSU_PASSWORD='your-password' \
28
+ -- uvx kitsu-mcp
29
+
30
+ The password is stored in plain text in the agent's config file (e.g. `~/.vibe/config.toml`).
31
+
32
+ ## Hosted connector (Le Chat, AI Studio)
33
+
34
+ Le Chat and AI Studio run in Mistral's cloud, so they need the server at a public HTTPS URL. One server serves
35
+ everyone: each person adds their own private connector with their Kitsu login, and every call runs as them,
36
+ with their Kitsu permissions.
37
+
38
+ **Run it.** Only `KITSU_HOST` is needed (no shared login). Deploy the image on a host that terminates HTTPS
39
+ (Fly.io, Render, a reverse proxy) and confirm it with `KITSU_BEHIND_TLS=true`:
40
+
41
+ docker run -p 8000:8000 -e KITSU_HOST=https://your-studio.cg-wire.com/api -e KITSU_BEHIND_TLS=true \
42
+ ghcr.io/estienneg/kitsu-mcp:latest
43
+
44
+ (or build it yourself with `docker build -t kitsu-mcp .`). Pin a version tag such as `:0.2.0` in production.
45
+
46
+ To try it locally without a proxy, run `KITSU_HOST=... kitsu-mcp --http`, which listens on 127.0.0.1:8000.
47
+
48
+ The supported entry point is `kitsu-mcp --http` (`--host` / `--port`, or `KITSU_HTTP_HOST` / `PORT`): it
49
+ applies the TLS check below, which `fastmcp run` would skip. The MCP endpoint is `/mcp`; `/health` answers
50
+ for health checks. The image runs as an unprivileged user.
51
+
52
+ **HTTPS is required.** Callers send their Kitsu password with every request, so the server refuses to listen
53
+ on anything but localhost unless the operator confirms TLS terminates in front of it (`--behind-tls` or
54
+ `KITSU_BEHIND_TLS=true`; deliberately not set in the image). It never logs credentials and keeps them only in
55
+ memory, under keys derived with a per-process secret. Only logins Kitsu accepted are kept, for an hour of
56
+ inactivity. Repeated rejected logins for one account are slowed down (a Kitsu outage does not count), while a
57
+ login Kitsu accepted in the last day is always let through, so guessing at someone's email cannot lock them
58
+ out. Spraying many emails from one address is not throttled; fine at studio scale, otherwise rate-limit at the
59
+ proxy. Listing the tools works without a login
60
+ (it is metadata only); every tool call needs one.
61
+
62
+ **Each user** (AI Studio → Connectors → Add, or Le Chat → Connectors → Custom MCP connector):
63
+ - URL: `https://<your-server>/mcp`, visibility **Private**
64
+ - Authentication: **Basic**, with their Kitsu email and password. Where only a bearer token can be entered,
65
+ use `email:password` as the token.
66
+
67
+ Differences from stdio: `download_previews` and `download_video` are hidden (they would write to the server's
68
+ disk, not the user's); give people the `url` (Kitsu page) or `download_url` (original upload) instead, which
69
+ open in their own Kitsu browser session. The
70
+ video tools cache movies per user, and `KITSU_VIDEO_CACHE_MB` caps all users' movies together. Write tools
71
+ follow `KITSU_ALLOW_WRITE` as over stdio. Kitsu accounts with two-factor authentication cannot log in this
72
+ way.
73
+
74
+ ## Configuration
75
+
76
+ | Variable | Default | |
77
+ |---|---|---|
78
+ | `KITSU_HOST` | required | Zou API URL, ending in `/api` |
79
+ | `KITSU_USER` / `KITSU_PASSWORD` | required over stdio | Kitsu login (over HTTP each caller sends their own) |
80
+ | `KITSU_ALLOW_WRITE` | `false` | Expose the write tools below |
81
+ | `KITSU_CACHE_TTL` | `300` | Seconds to cache projects, task types, statuses, people, departments, custom fields |
82
+ | `KITSU_TIMEOUT` | `30` | Per-request timeout (seconds) |
83
+ | `KITSU_MAX_RETRIES` | `3` | Attempts for reads failing with a server or network error |
84
+ | `KITSU_VIDEO_CACHE_DIR` | system temp | Where the video tools cache movies (`kitsu-mcp-videos/`) |
85
+ | `KITSU_VIDEO_CACHE_MB` | `1024` | Movie cache size; least recently used movies are deleted past it |
86
+ | `KITSU_VIDEO_MAX_MB` | `500` | Largest movie the video tools download |
87
+ | `KITSU_OMITTED_STATUSES` | `omit,na,n/a,omitted,cancelled,canceled` | Statuses meaning "dropped": left out of progress and open work |
88
+ | `KITSU_DONE_STATUSES` | (none) | Statuses meaning "finished", whatever Kitsu flags (e.g. `cbb` for "could be better") |
89
+ | `KITSU_ON_HOLD_STATUSES` | `stand by,standby,pause,paused,blocked,on hold` | Open but paused: never overdue |
90
+ | `KITSU_REVIEW_STATUSES` | (none) | Extra statuses meaning "waiting for review/approval" |
91
+ | `KITSU_RETAKE_STATUSES` | (none) | Extra statuses meaning "sent back for changes" |
92
+ | `KITSU_DRY_RUN_LOG` | (none) | JSONL file each write-tool dry run is appended to |
93
+
94
+ Variables can also come from a `.env` file in the working directory.
95
+
96
+ ### Status roles
97
+
98
+ Kitsu's flags on a status (done, retake, feedback request, in progress) do not always say what a studio
99
+ means: an OMIT status is often flagged done but means dropped, and a studio may flag a "could be better" (CBB)
100
+ status as retake while meaning done-but-improvable (set `KITSU_DONE_STATUSES=cbb` for that). So every status gets exactly one **role** — `omitted`, `done`, `on_hold`, `retake`,
101
+ `waiting_review`, `wip` or `todo` — and every count and filter uses it (progress, overdue, waiting for
102
+ review, workload, search). A status listed in one of the `KITSU_*_STATUSES` settings above (names or short
103
+ names, case-insensitive, checked in the order shown) takes that role; any other status gets it from
104
+ Kitsu's flags, in the order done > retake > feedback request > in progress > to do.
105
+
106
+ `list_task_statuses` shows each status's role and where it comes from, and reports configured names that
107
+ match no status. `project_overview` reports which statuses it counted as omitted and as
108
+ done-but-improvable.
109
+
110
+ ## Development
111
+
112
+ cp .env.example .env # fill in KITSU_USER / KITSU_PASSWORD
113
+ uv run kitsu-mcp # reads .env from the current directory
114
+ uv run pytest # tests run against an in-memory fake Kitsu, no credentials needed
115
+ uv run ruff check . && uv run ruff format --check . && uv run pyright
116
+
117
+ Layout (`src/kitsu_mcp/`): `config.py` settings, `client.py` HTTP client (login, re-login, retries,
118
+ error mapping), `lookups.py` cached reference data and name resolution, `models.py` output schemas,
119
+ `formatters.py` payload → model conversion, `production.py` project-wide task views, `plans.py` write plans
120
+ (dry run), `custom_fields.py` custom field values and filters, `tools/` one module per domain,
121
+ `resources.py`, `prompts.py`, `auth.py` per-caller credentials, `http.py` HTTP mode (hidden local tools,
122
+ `/health`, TLS guard), `links.py` Kitsu web links.
123
+
124
+ Release: bump `version` in `pyproject.toml`, merge, then tag it (`git tag v0.2.0 && git push origin v0.2.0`).
125
+ The Release workflow checks the tag matches the version, runs the checks, then publishes the package to
126
+ PyPI and the Docker image to `ghcr.io/estienneg/kitsu-mcp` (tags `0.2.0`, `0.2` and `latest`).
127
+ PyPI uses trusted publishing, so no token is stored: the `kitsu-mcp` project on PyPI has this repository
128
+ as its publisher (workflow `release.yml`, environment `pypi`).
129
+
130
+ ## Tools
131
+
132
+ Project, task type, department, status and person arguments take names (case-insensitive) or IDs.
133
+ `departments` filters (`search_tasks`, `workload`, `project_overview`) keep tasks whose task type belongs to
134
+ one of the departments, so the three tools agree; people's own departments are not used (`list_people` shows
135
+ them).
136
+
137
+ | Area | Tools |
138
+ |---|---|
139
+ | Meta | `whoami`, `kitsu_get` (raw read of any Zou `data/` route), `refresh_cache` |
140
+ | Projects | `list_projects`, `list_task_types`, `list_task_statuses`, `list_asset_types`, `project_overview` |
141
+ | Assets | `list_assets` (type, name and per-task-type status filters; `detail="compact"` or `"full"`), `get_asset`, `get_casting` (either way) |
142
+ | Shots | `list_episodes`, `list_sequences`, `list_shots`, `get_shot`, `get_casting` |
143
+ | Custom fields | `list_custom_fields` (a project's custom columns: type, choices, departments); values show up by display name in `get_asset` / `get_shot` (and `detail="full"` lists), and `list_assets` / `list_shots` filter on them with `custom_field_filters` |
144
+ | Tasks | `search_tasks` (type, department, status, assignee, due dates, overdue, waiting feedback, group by assignee, ...), `get_task`, `task_history`, `my_tasks` |
145
+ | People | `list_people`, `get_person_tasks`, `workload` |
146
+ | Previews | `recent_previews` (latest uploads), `get_preview` (image or metadata, by entity or preview file), `list_task_previews`, `download_previews`, `list_playlists`, `get_playlist` |
147
+ | Video (`[video]` extra) | `get_video_info`, `get_video_contact_sheet`, `get_video_frames`, `compare_revisions`, `review_video`, `download_video` |
148
+ | Write (opt-in) | `add_comment`, `set_task_status`, `assign_task`, `update_task_schedule`, `log_time`, `create_playlist`, `add_to_playlist` (shots/assets with their latest preview) |
149
+
150
+ **Links:** detail results carry a `url` to the Kitsu page (asset, shot, task, preview in the player, playlist),
151
+ and preview metadata a `download_url` to the original upload. They open in a browser signed in to Kitsu, so
152
+ movies are watched and downloaded there rather than passed through the server.
153
+
154
+ **Dry run.** Every write tool takes `dry_run=True`: it runs the same lookups and checks with reads only and
155
+ returns the plan instead of writing: `would` (the action in one line), `requests` (method, path and payload,
156
+ exactly what the real call sends), `before`/`after` for the fields that change (status, assignees, dates,
157
+ estimate, time logged), and `warnings` (e.g. Kitsu may refuse a status your role cannot set). If a check fails
158
+ (archived status, inactive person, start date after due date, over 24 h logged in a day), `verdict` is
159
+ `would_fail` with the `reason`, and the real call refuses with the same reason. Invalid arguments (bad IDs,
160
+ unknown names) are errors either way. Agents are told to dry-run first and show the user the plan.
161
+
162
+ **Resources:** `kitsu://projects`, `kitsu://task-types`, `kitsu://task-statuses`,
163
+ `kitsu://projects/{project}/overview`, `kitsu://tasks/{task_id}`.
164
+
165
+ **Prompts:** `project_status_report`, `review_queue`, `my_day`, `person_summary`, `review_shot`.
166
+
167
+ ## Video
168
+
169
+ The video tools look inside movie previews. They need the optional `video` extra (PyAV, which
170
+ bundles ffmpeg, and Pillow), so no system ffmpeg is required:
171
+
172
+ uvx --from 'kitsu-mcp[video]' kitsu-mcp # or: pip install 'kitsu-mcp[video]'
173
+
174
+ Use that as the command when registering the server (e.g. `-- uvx --from 'kitsu-mcp[video]' kitsu-mcp`
175
+ for Claude Code). Without the extra the tools are still listed but answer with an error saying how
176
+ to install it (`download_video` works either way). `get_preview` says when a
177
+ preview is a video.
178
+
179
+ - Movies are fetched at Kitsu's low-resolution playback quality by default (`quality="original"` for
180
+ the uploaded file) and cached per preview file and quality (see the `KITSU_VIDEO_*` settings).
181
+ - Tools take a `preview_file_id`, or an `entity_id` (shot or asset) to use its current preview.
182
+ - Frames are numbered from 1, like Kitsu's player. Frame arguments accept frame numbers (`37`),
183
+ timecodes (`"00:00:01:12"`, HH:MM:SS:FF), times (`"1.5s"`) and percentages (`"50%"`).
184
+ `review_video` finds the frames a task's comments point at: Kitsu's `v3 00:00:01:12 (37)` tags,
185
+ timecodes, "frame 37", "2.5s", and frames with drawn annotations.
186
+ - Limits: at most 16 frames per call, grids at most 1568 px on a side, single frames at most
187
+ 1280 px. The agent only sees sampled still frames: no motion in between and no audio. Every image
188
+ costs context, so start with `get_video_contact_sheet` and zoom in with `get_video_frames`.
189
+
190
+ ## License
191
+
192
+ MIT: see [LICENSE](LICENSE). Free to use, modify and distribute, including commercially, provided the
193
+ copyright notice is kept; no warranty.
@@ -0,0 +1,65 @@
1
+ [project]
2
+ name = "kitsu-mcp"
3
+ version = "0.3.0"
4
+ description = "MCP server for the Kitsu (CGWire) production tracker"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.12"
9
+ dependencies = [
10
+ "fastmcp>=4.1",
11
+ "gazu>=1.3.1",
12
+ "pydantic>=2.9",
13
+ "pydantic-settings>=2.6",
14
+ ]
15
+
16
+ [[project.authors]]
17
+ name = "EstienneG"
18
+ email = "enneitse@gmail.com"
19
+
20
+ [project.urls]
21
+ Repository = "https://github.com/EstienneG/kitsu-mcp"
22
+ Issues = "https://github.com/EstienneG/kitsu-mcp/issues"
23
+
24
+ [project.scripts]
25
+ kitsu-mcp = "kitsu_mcp:main"
26
+
27
+ [project.optional-dependencies]
28
+ video = [
29
+ "av>=14",
30
+ "pillow>=11",
31
+ ]
32
+
33
+ [build-system]
34
+ requires = ["uv_build>=0.12.5,<0.13.0"]
35
+ build-backend = "uv_build"
36
+
37
+ [dependency-groups]
38
+ dev = [
39
+ "kitsu-mcp[video]",
40
+ "pyright>=1.1.414",
41
+ "pytest>=9.1.1",
42
+ "pytest-asyncio>=1.4.0",
43
+ "ruff>=0.17.1",
44
+ ]
45
+
46
+ [tool.pytest.ini_options]
47
+ asyncio_mode = "auto"
48
+ testpaths = ["tests"]
49
+
50
+ [tool.ruff]
51
+ line-length = 120
52
+
53
+ [tool.ruff.lint]
54
+ select = [
55
+ "E",
56
+ "F",
57
+ "I",
58
+ "UP",
59
+ "B",
60
+ "SIM",
61
+ ]
62
+
63
+ [tool.pyright]
64
+ include = ["src"]
65
+ pythonVersion = "3.12"
@@ -0,0 +1,57 @@
1
+ [project]
2
+ name = "kitsu-mcp"
3
+ version = "0.3.0"
4
+ description = "MCP server for the Kitsu (CGWire) production tracker"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "EstienneG", email = "enneitse@gmail.com" }
10
+ ]
11
+ requires-python = ">=3.12"
12
+ dependencies = [
13
+ "fastmcp>=4.1",
14
+ "gazu>=1.3.1",
15
+ "pydantic>=2.9",
16
+ "pydantic-settings>=2.6",
17
+ ]
18
+
19
+ [project.urls]
20
+ Repository = "https://github.com/EstienneG/kitsu-mcp"
21
+ Issues = "https://github.com/EstienneG/kitsu-mcp/issues"
22
+
23
+ [project.scripts]
24
+ kitsu-mcp = "kitsu_mcp:main"
25
+
26
+ [project.optional-dependencies]
27
+ video = [
28
+ "av>=14",
29
+ "pillow>=11",
30
+ ]
31
+
32
+ [build-system]
33
+ requires = ["uv_build>=0.12.5,<0.13.0"]
34
+ build-backend = "uv_build"
35
+
36
+ [dependency-groups]
37
+ dev = [
38
+ "kitsu-mcp[video]",
39
+ "pyright>=1.1.414",
40
+ "pytest>=9.1.1",
41
+ "pytest-asyncio>=1.4.0",
42
+ "ruff>=0.17.1",
43
+ ]
44
+
45
+ [tool.pytest.ini_options]
46
+ asyncio_mode = "auto"
47
+ testpaths = ["tests"]
48
+
49
+ [tool.ruff]
50
+ line-length = 120
51
+
52
+ [tool.ruff.lint]
53
+ select = ["E", "F", "I", "UP", "B", "SIM"]
54
+
55
+ [tool.pyright]
56
+ include = ["src"]
57
+ pythonVersion = "3.12"
@@ -0,0 +1,33 @@
1
+ """MCP server for the Kitsu (CGWire) production tracker."""
2
+
3
+ import argparse
4
+ import os
5
+
6
+
7
+ def main() -> None:
8
+ parser = argparse.ArgumentParser(prog="kitsu-mcp", description=__doc__)
9
+ parser.add_argument("--http", action="store_true", help="serve over HTTP (hosted connector) instead of stdio")
10
+ parser.add_argument("--host", default=os.environ.get("KITSU_HTTP_HOST", "127.0.0.1"))
11
+ parser.add_argument("--port", type=int, default=int(os.environ.get("PORT", "8000")))
12
+ parser.add_argument(
13
+ "--behind-tls",
14
+ action="store_true",
15
+ default=os.environ.get("KITSU_BEHIND_TLS", "").lower() in ("1", "true", "yes"),
16
+ help="confirm HTTPS terminates in front of the server (required to bind a non-loopback host)",
17
+ )
18
+ args = parser.parse_args()
19
+
20
+ from kitsu_mcp.config import get_settings, require_login
21
+ from kitsu_mcp.server import mcp
22
+
23
+ settings = get_settings()
24
+ if not args.http:
25
+ require_login(settings)
26
+ if settings.allow_write:
27
+ mcp.enable(tags={"write"})
28
+ if args.http:
29
+ from kitsu_mcp import http
30
+
31
+ http.run(args.host, args.port, args.behind_tls)
32
+ else:
33
+ mcp.run()
@@ -0,0 +1,3 @@
1
+ from kitsu_mcp import main
2
+
3
+ main()
@@ -0,0 +1,45 @@
1
+ """The shared FastMCP instance. Tool modules register on it when imported."""
2
+
3
+ import inspect
4
+
5
+ from fastmcp import FastMCP
6
+
7
+ mcp = FastMCP(
8
+ "kitsu",
9
+ instructions=(
10
+ "Access to a Kitsu production tracker. Tools accept project, task type, status and "
11
+ "person names as well as IDs (case-insensitive). Status filters use task status short "
12
+ "names (e.g. wip, wfa, done); list_task_statuses shows them and the role each one plays here "
13
+ "(done, omitted, on hold, waiting review, ...), which every count and filter follows. Durations "
14
+ 'and estimates are in minutes. List tools return compact rows by default (detail="full" for '
15
+ "more). Use kitsu_get for any read the other tools don't cover. Write tools "
16
+ "(comments, status changes, assignments, schedule, time logs, playlists) only exist when the server "
17
+ "runs with KITSU_ALLOW_WRITE=true. For write tools, call with dry_run=True first and show the user the "
18
+ "plan before writing; then confirm with the user before calling them for real."
19
+ ),
20
+ )
21
+
22
+ READ_ONLY = {"readOnlyHint": True, "openWorldHint": True}
23
+
24
+
25
+ # One small schema for every data tool instead of a detailed one per tool: clients still get structured
26
+ # results (fastmcp's `.data`, `structured_content`), while the tool list stays about half the size.
27
+ _GENERIC_OUTPUT = {"type": "object", "properties": {"result": {}}, "x-fastmcp-wrap-result": True}
28
+
29
+
30
+ def tool(**kwargs):
31
+ """Register a tool on `mcp` with a generic output schema instead of a detailed one.
32
+
33
+ Detailed output schemas were half of what every session paid in context before any call; the field
34
+ notes that matter are in the docstrings. Tools that can return images keep no schema at all, since
35
+ images cannot be structured content. The decorated function itself is returned unchanged, so other
36
+ modules (resources) can still call it directly.
37
+ """
38
+
39
+ def register(fn):
40
+ returns = str(inspect.signature(fn).return_annotation)
41
+ schema = None if "Image" in returns else _GENERIC_OUTPUT
42
+ mcp.tool(output_schema=schema, **kwargs)(fn)
43
+ return fn
44
+
45
+ return register