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.
- kitsu_mcp-0.3.0/LICENSE +21 -0
- kitsu_mcp-0.3.0/PKG-INFO +213 -0
- kitsu_mcp-0.3.0/README.md +193 -0
- kitsu_mcp-0.3.0/pyproject.toml +65 -0
- kitsu_mcp-0.3.0/pyproject.toml.orig +57 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/__init__.py +33 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/__main__.py +3 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/app.py +45 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/auth.py +177 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/client.py +244 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/config.py +77 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/custom_fields.py +155 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/formatters.py +67 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/http.py +78 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/links.py +79 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/lookups.py +240 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/models.py +534 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/plans.py +101 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/production.py +109 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/prompts.py +70 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/resources.py +36 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/server.py +9 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/tools/__init__.py +5 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/tools/assets.py +157 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/tools/custom_fields.py +34 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/tools/meta.py +149 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/tools/people.py +143 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/tools/previews.py +300 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/tools/projects.py +203 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/tools/shots.py +196 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/tools/tasks.py +191 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/tools/video.py +341 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/tools/write.py +472 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/validation.py +41 -0
- kitsu_mcp-0.3.0/src/kitsu_mcp/video.py +706 -0
kitsu_mcp-0.3.0/LICENSE
ADDED
|
@@ -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.
|
kitsu_mcp-0.3.0/PKG-INFO
ADDED
|
@@ -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,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
|