krita-cli 1.0.0__py3-none-any.whl
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.
- krita_cli/__init__.py +5 -0
- krita_cli/_shared.py +94 -0
- krita_cli/app.py +149 -0
- krita_cli/cli.py +59 -0
- krita_cli/commands/__init__.py +1 -0
- krita_cli/commands/batch.py +86 -0
- krita_cli/commands/brush.py +58 -0
- krita_cli/commands/call.py +49 -0
- krita_cli/commands/canvas.py +77 -0
- krita_cli/commands/color.py +42 -0
- krita_cli/commands/config.py +48 -0
- krita_cli/commands/file_ops.py +27 -0
- krita_cli/commands/health.py +32 -0
- krita_cli/commands/history_cmd.py +64 -0
- krita_cli/commands/introspect.py +46 -0
- krita_cli/commands/layers.py +112 -0
- krita_cli/commands/navigation.py +33 -0
- krita_cli/commands/replay.py +126 -0
- krita_cli/commands/rollback.py +39 -0
- krita_cli/commands/selection.py +364 -0
- krita_cli/commands/stroke.py +101 -0
- krita_cli/config_cmd.py +68 -0
- krita_cli/history.py +198 -0
- krita_cli-1.0.0.dist-info/METADATA +103 -0
- krita_cli-1.0.0.dist-info/RECORD +35 -0
- krita_cli-1.0.0.dist-info/WHEEL +4 -0
- krita_cli-1.0.0.dist-info/entry_points.txt +2 -0
- krita_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- krita_client/__init__.py +68 -0
- krita_client/client.py +690 -0
- krita_client/config.py +53 -0
- krita_client/models.py +507 -0
- krita_client/schema.py +109 -0
- krita_mcp/__init__.py +1 -0
- krita_mcp/server.py +1022 -0
krita_mcp/server.py
ADDED
|
@@ -0,0 +1,1022 @@
|
|
|
1
|
+
"""Krita MCP Server — FastMCP interface for AI agents.
|
|
2
|
+
|
|
3
|
+
Exposes painting tools to any MCP client (Claude, etc.) by wrapping
|
|
4
|
+
the krita_client library.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, cast
|
|
10
|
+
|
|
11
|
+
from fastmcp import FastMCP
|
|
12
|
+
|
|
13
|
+
from krita_client import (
|
|
14
|
+
ClientConfig,
|
|
15
|
+
KritaClient,
|
|
16
|
+
KritaConnectionError,
|
|
17
|
+
KritaError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Configuration
|
|
21
|
+
_client: KritaClient | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_client() -> KritaClient:
|
|
25
|
+
"""Get or create the Krita client singleton."""
|
|
26
|
+
global _client
|
|
27
|
+
if _client is None:
|
|
28
|
+
config = ClientConfig()
|
|
29
|
+
_client = KritaClient(config)
|
|
30
|
+
return _client
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _format_error(exc: KritaError) -> str:
|
|
34
|
+
"""Format a Krita error for MCP response."""
|
|
35
|
+
if exc.code:
|
|
36
|
+
return f"[{exc.code}] {exc.message}"
|
|
37
|
+
return exc.message
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
mcp = FastMCP("krita-mcp")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@mcp.tool()
|
|
44
|
+
def krita_health() -> str:
|
|
45
|
+
"""Check if Krita is running and the MCP plugin is active."""
|
|
46
|
+
try:
|
|
47
|
+
client = _get_client()
|
|
48
|
+
result = client.health()
|
|
49
|
+
if "error" in result:
|
|
50
|
+
return f"Error: {result['error']}"
|
|
51
|
+
plugin = result.get("plugin", "unknown")
|
|
52
|
+
status = result.get("status", "unknown")
|
|
53
|
+
return f"Krita is running. Plugin: {plugin} ({status})"
|
|
54
|
+
except KritaConnectionError as exc:
|
|
55
|
+
return f"Cannot connect to Krita. Make sure Krita is running with the MCP plugin enabled. ({exc.message})"
|
|
56
|
+
except KritaError as exc:
|
|
57
|
+
return _format_error(exc)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@mcp.tool()
|
|
61
|
+
def krita_new_canvas(
|
|
62
|
+
width: int = 800,
|
|
63
|
+
height: int = 600,
|
|
64
|
+
name: str = "New Canvas",
|
|
65
|
+
background: str = "#1a1a2e",
|
|
66
|
+
) -> str:
|
|
67
|
+
"""Create a new canvas in Krita.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
width: Canvas width in pixels (default 800, max 8192)
|
|
71
|
+
height: Canvas height in pixels (default 600, max 8192)
|
|
72
|
+
name: Document name
|
|
73
|
+
background: Background color as hex (default dark blue)
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
client = _get_client()
|
|
77
|
+
result = client.new_canvas(
|
|
78
|
+
width=width,
|
|
79
|
+
height=height,
|
|
80
|
+
name=name,
|
|
81
|
+
background=background,
|
|
82
|
+
)
|
|
83
|
+
if "error" in result:
|
|
84
|
+
return f"Error: {result['error']}"
|
|
85
|
+
w = result.get("width", width)
|
|
86
|
+
h = result.get("height", height)
|
|
87
|
+
return f"Created canvas: {w}x{h}, background: {background}"
|
|
88
|
+
except KritaError as exc:
|
|
89
|
+
return _format_error(exc)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@mcp.tool()
|
|
93
|
+
def krita_set_color(color: str) -> str:
|
|
94
|
+
"""Set the foreground paint color.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
color: Hex color code (e.g., "#ff6b6b", "#b8a9c9")
|
|
98
|
+
"""
|
|
99
|
+
try:
|
|
100
|
+
client = _get_client()
|
|
101
|
+
result = client.set_color(color=color)
|
|
102
|
+
if "error" in result:
|
|
103
|
+
return f"Error: {result['error']}"
|
|
104
|
+
return f"Color set to {color}"
|
|
105
|
+
except KritaError as exc:
|
|
106
|
+
return _format_error(exc)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@mcp.tool()
|
|
110
|
+
def krita_set_brush(
|
|
111
|
+
preset: str | None = None,
|
|
112
|
+
size: int | None = None,
|
|
113
|
+
opacity: float | None = None,
|
|
114
|
+
) -> str:
|
|
115
|
+
"""Set brush preset and properties.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
preset: Brush preset name (partial match, e.g., "Basic", "Soft", "Airbrush")
|
|
119
|
+
size: Brush size in pixels
|
|
120
|
+
opacity: Brush opacity (0.0 to 1.0)
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
client = _get_client()
|
|
124
|
+
result = client.set_brush(preset=preset, size=size, opacity=opacity)
|
|
125
|
+
if "error" in result:
|
|
126
|
+
return f"Error: {result['error']}"
|
|
127
|
+
parts = []
|
|
128
|
+
if preset:
|
|
129
|
+
parts.append(f"preset={preset}")
|
|
130
|
+
if size is not None:
|
|
131
|
+
parts.append(f"size={size}")
|
|
132
|
+
if opacity is not None:
|
|
133
|
+
parts.append(f"opacity={opacity}")
|
|
134
|
+
return f"Brush set: {', '.join(parts) if parts else 'no changes'}"
|
|
135
|
+
except KritaError as exc:
|
|
136
|
+
return _format_error(exc)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@mcp.tool()
|
|
140
|
+
def krita_stroke(
|
|
141
|
+
points: list[list[int]],
|
|
142
|
+
pressure: float = 1.0,
|
|
143
|
+
size: int | None = None,
|
|
144
|
+
hardness: float = 0.5,
|
|
145
|
+
opacity: float = 1.0,
|
|
146
|
+
) -> str:
|
|
147
|
+
"""Paint a stroke through a series of points.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
points: List of [x, y] coordinate pairs, e.g., [[100, 100], [150, 120], [200, 150]]
|
|
151
|
+
pressure: Brush pressure (0.0 to 1.0, affects stroke thickness/opacity)
|
|
152
|
+
size: Brush size in pixels (overrides current brush size)
|
|
153
|
+
hardness: Stroke hardness (0.0 = very soft, 1.0 = hard edge)
|
|
154
|
+
opacity: Stroke opacity (0.0 to 1.0)
|
|
155
|
+
"""
|
|
156
|
+
try:
|
|
157
|
+
client = _get_client()
|
|
158
|
+
result = client.stroke(
|
|
159
|
+
points=points,
|
|
160
|
+
pressure=pressure,
|
|
161
|
+
size=size,
|
|
162
|
+
hardness=hardness,
|
|
163
|
+
opacity=opacity,
|
|
164
|
+
)
|
|
165
|
+
if "error" in result:
|
|
166
|
+
return f"Error: {result['error']}"
|
|
167
|
+
return f"Stroke painted with {len(points)} points"
|
|
168
|
+
except KritaError as exc:
|
|
169
|
+
return _format_error(exc)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@mcp.tool()
|
|
173
|
+
def krita_fill(x: int, y: int, radius: int = 50) -> str:
|
|
174
|
+
"""Fill an area with current color (paints a filled circle at the point).
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
x: X coordinate
|
|
178
|
+
y: Y coordinate
|
|
179
|
+
radius: Fill radius in pixels
|
|
180
|
+
"""
|
|
181
|
+
try:
|
|
182
|
+
client = _get_client()
|
|
183
|
+
result = client.fill(x=x, y=y, radius=radius)
|
|
184
|
+
if "error" in result:
|
|
185
|
+
return f"Error: {result['error']}"
|
|
186
|
+
return f"Filled at ({x}, {y}) with radius {radius}"
|
|
187
|
+
except KritaError as exc:
|
|
188
|
+
return _format_error(exc)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@mcp.tool()
|
|
192
|
+
def krita_draw_shape(
|
|
193
|
+
shape: str,
|
|
194
|
+
x: int,
|
|
195
|
+
y: int,
|
|
196
|
+
width: int = 100,
|
|
197
|
+
height: int = 100,
|
|
198
|
+
fill: bool = True,
|
|
199
|
+
stroke: bool = False,
|
|
200
|
+
x2: int | None = None,
|
|
201
|
+
y2: int | None = None,
|
|
202
|
+
) -> str:
|
|
203
|
+
"""Draw a shape on the canvas.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
shape: Type of shape - "rectangle", "ellipse", or "line"
|
|
207
|
+
x: X coordinate (top-left for shapes, start point for lines)
|
|
208
|
+
y: Y coordinate (top-left for shapes, start point for lines)
|
|
209
|
+
width: Width of shape (ignored for lines if x2/y2 provided)
|
|
210
|
+
height: Height of shape (ignored for lines if x2/y2 provided)
|
|
211
|
+
fill: Whether to fill the shape
|
|
212
|
+
stroke: Whether to draw outline
|
|
213
|
+
x2: End X for lines (optional)
|
|
214
|
+
y2: End Y for lines (optional)
|
|
215
|
+
"""
|
|
216
|
+
try:
|
|
217
|
+
client = _get_client()
|
|
218
|
+
result = client.draw_shape(
|
|
219
|
+
shape=shape,
|
|
220
|
+
x=x,
|
|
221
|
+
y=y,
|
|
222
|
+
width=width,
|
|
223
|
+
height=height,
|
|
224
|
+
fill=fill,
|
|
225
|
+
stroke=stroke,
|
|
226
|
+
x2=x2,
|
|
227
|
+
y2=y2,
|
|
228
|
+
)
|
|
229
|
+
if "error" in result:
|
|
230
|
+
return f"Error: {result['error']}"
|
|
231
|
+
return f"Drew {shape} at ({x}, {y})"
|
|
232
|
+
except KritaError as exc:
|
|
233
|
+
return _format_error(exc)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@mcp.tool()
|
|
237
|
+
def krita_get_canvas(filename: str = "canvas.png") -> str:
|
|
238
|
+
"""Export current canvas to a PNG file and return the path.
|
|
239
|
+
Use this to see your painting progress.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
filename: Output filename (saved to configured output directory)
|
|
243
|
+
"""
|
|
244
|
+
try:
|
|
245
|
+
client = _get_client()
|
|
246
|
+
result = client.get_canvas(filename=filename)
|
|
247
|
+
if "error" in result:
|
|
248
|
+
return f"Error: {result['error']}"
|
|
249
|
+
path = result.get("path", "")
|
|
250
|
+
return f"Canvas saved to: {path}"
|
|
251
|
+
except KritaError as exc:
|
|
252
|
+
return _format_error(exc)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@mcp.tool()
|
|
256
|
+
def krita_undo() -> str:
|
|
257
|
+
"""Undo the last action."""
|
|
258
|
+
try:
|
|
259
|
+
client = _get_client()
|
|
260
|
+
result = client.undo()
|
|
261
|
+
if "error" in result:
|
|
262
|
+
return f"Error: {result['error']}"
|
|
263
|
+
return "Undone"
|
|
264
|
+
except KritaError as exc:
|
|
265
|
+
return _format_error(exc)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@mcp.tool()
|
|
269
|
+
def krita_redo() -> str:
|
|
270
|
+
"""Redo the last undone action."""
|
|
271
|
+
try:
|
|
272
|
+
client = _get_client()
|
|
273
|
+
result = client.redo()
|
|
274
|
+
if "error" in result:
|
|
275
|
+
return f"Error: {result['error']}"
|
|
276
|
+
return "Redone"
|
|
277
|
+
except KritaError as exc:
|
|
278
|
+
return _format_error(exc)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@mcp.tool()
|
|
282
|
+
def krita_clear(color: str = "#1a1a2e") -> str:
|
|
283
|
+
"""Clear the canvas to a solid color.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
color: Color to fill canvas with (default dark blue)
|
|
287
|
+
"""
|
|
288
|
+
try:
|
|
289
|
+
client = _get_client()
|
|
290
|
+
result = client.clear(color=color)
|
|
291
|
+
if "error" in result:
|
|
292
|
+
return f"Error: {result['error']}"
|
|
293
|
+
return f"Canvas cleared to {color}"
|
|
294
|
+
except KritaError as exc:
|
|
295
|
+
return _format_error(exc)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@mcp.tool()
|
|
299
|
+
def krita_save(path: str) -> str:
|
|
300
|
+
"""Save the current canvas to a specific file path.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
path: Full file path to save to (e.g., "C:/art/my_painting.png")
|
|
304
|
+
"""
|
|
305
|
+
try:
|
|
306
|
+
client = _get_client()
|
|
307
|
+
result = client.save(path=path)
|
|
308
|
+
if "error" in result:
|
|
309
|
+
return f"Error: {result['error']}"
|
|
310
|
+
return f"Saved to {path}"
|
|
311
|
+
except KritaError as exc:
|
|
312
|
+
return _format_error(exc)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@mcp.tool()
|
|
316
|
+
def krita_get_color_at(x: int, y: int) -> str:
|
|
317
|
+
"""Sample the color at a specific pixel (eyedropper).
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
x: X coordinate
|
|
321
|
+
y: Y coordinate
|
|
322
|
+
"""
|
|
323
|
+
try:
|
|
324
|
+
client = _get_client()
|
|
325
|
+
result = client.get_color_at(x=x, y=y)
|
|
326
|
+
if "error" in result:
|
|
327
|
+
return f"Error: {result['error']}"
|
|
328
|
+
color = result.get("color", "unknown")
|
|
329
|
+
r = result.get("r")
|
|
330
|
+
g = result.get("g")
|
|
331
|
+
b = result.get("b")
|
|
332
|
+
return f"Color at ({x}, {y}): {color} (R:{r}, G:{g}, B:{b})"
|
|
333
|
+
except KritaError as exc:
|
|
334
|
+
return _format_error(exc)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@mcp.tool()
|
|
338
|
+
def krita_list_brushes(filter: str = "", limit: int = 20) -> str:
|
|
339
|
+
"""List available brush presets.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
filter: Filter brushes by name (partial match)
|
|
343
|
+
limit: Maximum number to return
|
|
344
|
+
"""
|
|
345
|
+
try:
|
|
346
|
+
client = _get_client()
|
|
347
|
+
result = client.list_brushes(filter=filter, limit=limit)
|
|
348
|
+
if "error" in result:
|
|
349
|
+
return f"Error: {result['error']}"
|
|
350
|
+
brushes_raw = result.get("brushes", [])
|
|
351
|
+
if not isinstance(brushes_raw, list):
|
|
352
|
+
brushes_raw = []
|
|
353
|
+
brushes = [str(b) for b in brushes_raw]
|
|
354
|
+
if not brushes:
|
|
355
|
+
return "No brushes found matching filter"
|
|
356
|
+
return f"Available brushes ({len(brushes)}):\n" + "\n".join(f" - {b}" for b in brushes)
|
|
357
|
+
except KritaError as exc:
|
|
358
|
+
return _format_error(exc)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@mcp.tool()
|
|
362
|
+
def krita_open_file(path: str) -> str:
|
|
363
|
+
"""Open an existing file in Krita (.kra, .png, .jpg, etc).
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
path: Full file path to open (e.g., "C:/art/my_painting.kra")
|
|
367
|
+
"""
|
|
368
|
+
try:
|
|
369
|
+
client = _get_client()
|
|
370
|
+
result = client.open_file(path=path)
|
|
371
|
+
if "error" in result:
|
|
372
|
+
return f"Error: {result['error']}"
|
|
373
|
+
name = result.get("name", "unknown")
|
|
374
|
+
w = result.get("width")
|
|
375
|
+
h = result.get("height")
|
|
376
|
+
return f"Opened: {name} ({w}x{h})"
|
|
377
|
+
except KritaError as exc:
|
|
378
|
+
return _format_error(exc)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@mcp.tool()
|
|
382
|
+
def krita_batch(
|
|
383
|
+
commands: list[dict],
|
|
384
|
+
stop_on_error: bool = False,
|
|
385
|
+
) -> str:
|
|
386
|
+
"""Execute multiple commands in a single batch.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
commands: List of command objects, each with "action" and optional "params"
|
|
390
|
+
stop_on_error: Stop executing remaining commands on first error
|
|
391
|
+
"""
|
|
392
|
+
try:
|
|
393
|
+
client = _get_client()
|
|
394
|
+
result = client.batch_execute(commands, stop_on_error=stop_on_error)
|
|
395
|
+
if "error" in result:
|
|
396
|
+
return f"Error: {result['error']}"
|
|
397
|
+
|
|
398
|
+
results_raw = result.get("results", [])
|
|
399
|
+
if not isinstance(results_raw, list):
|
|
400
|
+
results_raw = []
|
|
401
|
+
results = cast("list[dict[str, Any]]", results_raw)
|
|
402
|
+
|
|
403
|
+
ok = sum(1 for r in results if isinstance(r, dict) and r.get("status") == "ok")
|
|
404
|
+
errs = sum(1 for r in results if isinstance(r, dict) and r.get("status") == "error")
|
|
405
|
+
summary = f"Batch: {ok} succeeded, {errs} failed out of {len(results)}"
|
|
406
|
+
|
|
407
|
+
batch_id = result.get("batch_id")
|
|
408
|
+
if batch_id:
|
|
409
|
+
summary += f" (Batch ID: {batch_id})"
|
|
410
|
+
|
|
411
|
+
if errs > 0:
|
|
412
|
+
error_details = []
|
|
413
|
+
for r in results:
|
|
414
|
+
if isinstance(r, dict) and r.get("status") == "error":
|
|
415
|
+
err_msg = _extract_batch_error(r)
|
|
416
|
+
error_details.append(f" - {r.get('action', 'unknown')}: {err_msg}")
|
|
417
|
+
if error_details:
|
|
418
|
+
summary += "\nErrors:\n" + "\n".join(error_details)
|
|
419
|
+
return summary
|
|
420
|
+
except KritaError as exc:
|
|
421
|
+
return _format_error(exc)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _extract_batch_error(r: dict[str, Any]) -> str:
|
|
425
|
+
"""Extract error message from a batch result entry."""
|
|
426
|
+
err_msg = r.get("error")
|
|
427
|
+
if not err_msg:
|
|
428
|
+
result_data = r.get("result", {})
|
|
429
|
+
if isinstance(result_data, dict) and "error" in result_data:
|
|
430
|
+
err_info = result_data["error"]
|
|
431
|
+
err_msg = err_info.get("message", str(err_info)) if isinstance(err_info, dict) else str(err_info)
|
|
432
|
+
|
|
433
|
+
if not err_msg:
|
|
434
|
+
err_msg = "unknown"
|
|
435
|
+
return str(err_msg)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@mcp.tool()
|
|
439
|
+
def krita_rollback(
|
|
440
|
+
batch_id: str,
|
|
441
|
+
) -> str:
|
|
442
|
+
"""Roll back a previously executed batch operation.
|
|
443
|
+
|
|
444
|
+
This restores the canvas state to what it was before the batch started.
|
|
445
|
+
Note: Snapshots are lost if the Krita plugin is restarted.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
batch_id: The unique ID returned by a previous krita_batch call.
|
|
449
|
+
"""
|
|
450
|
+
try:
|
|
451
|
+
client = _get_client()
|
|
452
|
+
result = client.rollback(batch_id=batch_id)
|
|
453
|
+
if "error" in result:
|
|
454
|
+
return f"Error: {result['error']}"
|
|
455
|
+
msg = result.get("message", "Rollback successful")
|
|
456
|
+
return f"Rollback complete: {msg}"
|
|
457
|
+
except KritaError as exc:
|
|
458
|
+
return _format_error(exc)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@mcp.tool()
|
|
462
|
+
def krita_get_command_history(
|
|
463
|
+
limit: int = 20,
|
|
464
|
+
) -> str:
|
|
465
|
+
"""Get recent command execution history.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
limit: Number of history entries to return (default 20)
|
|
469
|
+
"""
|
|
470
|
+
try:
|
|
471
|
+
client = _get_client()
|
|
472
|
+
result = client.get_command_history(limit=limit)
|
|
473
|
+
if "error" in result:
|
|
474
|
+
return f"Error: {result['error']}"
|
|
475
|
+
records_raw = result.get("history", [])
|
|
476
|
+
if not isinstance(records_raw, list):
|
|
477
|
+
records_raw = []
|
|
478
|
+
records = cast("list[dict[str, Any]]", records_raw)
|
|
479
|
+
|
|
480
|
+
if not records:
|
|
481
|
+
return "No command history recorded."
|
|
482
|
+
lines = [f"Command History ({len(records)} entries):"]
|
|
483
|
+
for i, rec in enumerate(records, 1):
|
|
484
|
+
status = rec.get("status", "?")
|
|
485
|
+
action = rec.get("action", "?")
|
|
486
|
+
duration = rec.get("duration_ms", 0)
|
|
487
|
+
error = rec.get("error", "")
|
|
488
|
+
line = f" {i}. {action} — {status} ({duration:.1f}ms)"
|
|
489
|
+
if error:
|
|
490
|
+
line += f" — {error}"
|
|
491
|
+
lines.append(line)
|
|
492
|
+
return "\n".join(lines)
|
|
493
|
+
except KritaError as exc:
|
|
494
|
+
return _format_error(exc)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@mcp.tool()
|
|
498
|
+
def krita_get_canvas_info() -> str:
|
|
499
|
+
"""Get information about the current canvas including dimensions, name, and color model.
|
|
500
|
+
|
|
501
|
+
Use this to understand the canvas you are working with.
|
|
502
|
+
"""
|
|
503
|
+
try:
|
|
504
|
+
client = _get_client()
|
|
505
|
+
result = client.get_canvas_info()
|
|
506
|
+
if "error" in result:
|
|
507
|
+
return f"Error: {result['error']}"
|
|
508
|
+
parts = []
|
|
509
|
+
if "name" in result:
|
|
510
|
+
parts.append(f"name={result['name']}")
|
|
511
|
+
if "width" in result:
|
|
512
|
+
parts.append(f"width={result['width']}")
|
|
513
|
+
if "height" in result:
|
|
514
|
+
parts.append(f"height={result['height']}")
|
|
515
|
+
if "color_model" in result:
|
|
516
|
+
parts.append(f"color_model={result['color_model']}")
|
|
517
|
+
if "color_depth" in result:
|
|
518
|
+
parts.append(f"color_depth={result['color_depth']}")
|
|
519
|
+
return f"Canvas info: {', '.join(parts) if parts else 'no active document'}"
|
|
520
|
+
except KritaError as exc:
|
|
521
|
+
return _format_error(exc)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
@mcp.tool()
|
|
525
|
+
def krita_get_current_color() -> str:
|
|
526
|
+
"""Get the current foreground and background paint colors.
|
|
527
|
+
|
|
528
|
+
Use this to check what colors are currently selected for painting.
|
|
529
|
+
"""
|
|
530
|
+
try:
|
|
531
|
+
client = _get_client()
|
|
532
|
+
result = client.get_current_color()
|
|
533
|
+
if "error" in result:
|
|
534
|
+
return f"Error: {result['error']}"
|
|
535
|
+
fg = result.get("foreground", "unknown")
|
|
536
|
+
bg = result.get("background", "unknown")
|
|
537
|
+
return f"Colors — foreground: {fg}, background: {bg}"
|
|
538
|
+
except KritaError as exc:
|
|
539
|
+
return _format_error(exc)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
@mcp.tool()
|
|
543
|
+
def krita_get_current_brush() -> str:
|
|
544
|
+
"""Get the current brush preset name, size, and opacity.
|
|
545
|
+
|
|
546
|
+
Use this to check the active brush settings before painting.
|
|
547
|
+
"""
|
|
548
|
+
try:
|
|
549
|
+
client = _get_client()
|
|
550
|
+
result = client.get_current_brush()
|
|
551
|
+
if "error" in result:
|
|
552
|
+
return f"Error: {result['error']}"
|
|
553
|
+
parts = []
|
|
554
|
+
if "preset" in result:
|
|
555
|
+
parts.append(f"preset={result['preset']}")
|
|
556
|
+
if "size" in result:
|
|
557
|
+
parts.append(f"size={result['size']}")
|
|
558
|
+
if "opacity" in result:
|
|
559
|
+
parts.append(f"opacity={result['opacity']}")
|
|
560
|
+
return f"Brush info: {', '.join(parts) if parts else 'no active view'}"
|
|
561
|
+
except KritaError as exc:
|
|
562
|
+
return _format_error(exc)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
@mcp.tool()
|
|
566
|
+
def krita_select_rect(x: int, y: int, width: int, height: int) -> str:
|
|
567
|
+
"""Select a rectangular area on the canvas.
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
x: X coordinate of top-left corner
|
|
571
|
+
y: Y coordinate of top-left corner
|
|
572
|
+
width: Width of the selection
|
|
573
|
+
height: Height of the selection
|
|
574
|
+
"""
|
|
575
|
+
try:
|
|
576
|
+
client = _get_client()
|
|
577
|
+
result = client.select_rect(x=x, y=y, width=width, height=height)
|
|
578
|
+
if "error" in result:
|
|
579
|
+
return f"Error: {result['error']}"
|
|
580
|
+
return f"Selected rectangle {width}x{height} at ({x}, {y})"
|
|
581
|
+
except KritaError as exc:
|
|
582
|
+
return _format_error(exc)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
@mcp.tool()
|
|
586
|
+
def krita_select_ellipse(cx: int, cy: int, rx: int, ry: int) -> str:
|
|
587
|
+
"""Select an elliptical area on the canvas.
|
|
588
|
+
|
|
589
|
+
Args:
|
|
590
|
+
cx: X coordinate of center
|
|
591
|
+
cy: Y coordinate of center
|
|
592
|
+
rx: Horizontal radius
|
|
593
|
+
ry: Vertical radius
|
|
594
|
+
"""
|
|
595
|
+
try:
|
|
596
|
+
client = _get_client()
|
|
597
|
+
result = client.select_ellipse(cx=cx, cy=cy, rx=rx, ry=ry)
|
|
598
|
+
if "error" in result:
|
|
599
|
+
return f"Error: {result['error']}"
|
|
600
|
+
return f"Selected ellipse at ({cx}, {cy}) with radii {rx}x{ry}"
|
|
601
|
+
except KritaError as exc:
|
|
602
|
+
return _format_error(exc)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
@mcp.tool()
|
|
606
|
+
def krita_select_polygon(points: list[list[int]]) -> str:
|
|
607
|
+
"""Select a polygonal area on the canvas.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
points: List of [x, y] coordinate pairs (minimum 3 points)
|
|
611
|
+
"""
|
|
612
|
+
try:
|
|
613
|
+
client = _get_client()
|
|
614
|
+
result = client.select_polygon(points=points)
|
|
615
|
+
if "error" in result:
|
|
616
|
+
return f"Error: {result['error']}"
|
|
617
|
+
return f"Selected polygon with {len(points)} points"
|
|
618
|
+
except KritaError as exc:
|
|
619
|
+
return _format_error(exc)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
@mcp.tool()
|
|
623
|
+
def krita_selection_info() -> str:
|
|
624
|
+
"""Get information about the current selection."""
|
|
625
|
+
try:
|
|
626
|
+
client = _get_client()
|
|
627
|
+
result = client.selection_info()
|
|
628
|
+
if "error" in result:
|
|
629
|
+
return f"Error: {result['error']}"
|
|
630
|
+
if result.get("has_selection"):
|
|
631
|
+
bounds_raw = result.get("bounds", {})
|
|
632
|
+
if not isinstance(bounds_raw, dict):
|
|
633
|
+
bounds_raw = {}
|
|
634
|
+
b = cast("dict[str, Any]", bounds_raw)
|
|
635
|
+
return f"Selection: x={b.get('x')}, y={b.get('y')}, w={b.get('width')}, h={b.get('height')}"
|
|
636
|
+
return "No active selection"
|
|
637
|
+
except KritaError as exc:
|
|
638
|
+
return _format_error(exc)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
@mcp.tool()
|
|
642
|
+
def krita_clear_selection() -> str:
|
|
643
|
+
"""Clear the content of the current selection."""
|
|
644
|
+
try:
|
|
645
|
+
client = _get_client()
|
|
646
|
+
result = client.clear_selection()
|
|
647
|
+
if "error" in result:
|
|
648
|
+
return f"Error: {result['error']}"
|
|
649
|
+
return "Cleared selection"
|
|
650
|
+
except KritaError as exc:
|
|
651
|
+
return _format_error(exc)
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
@mcp.tool()
|
|
655
|
+
def krita_invert_selection() -> str:
|
|
656
|
+
"""Invert the current selection."""
|
|
657
|
+
try:
|
|
658
|
+
client = _get_client()
|
|
659
|
+
result = client.invert_selection()
|
|
660
|
+
if "error" in result:
|
|
661
|
+
return f"Error: {result['error']}"
|
|
662
|
+
return "Inverted selection"
|
|
663
|
+
except KritaError as exc:
|
|
664
|
+
return _format_error(exc)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
@mcp.tool()
|
|
668
|
+
def krita_fill_selection() -> str:
|
|
669
|
+
"""Fill the current selection with the foreground color."""
|
|
670
|
+
try:
|
|
671
|
+
client = _get_client()
|
|
672
|
+
result = client.fill_selection()
|
|
673
|
+
if "error" in result:
|
|
674
|
+
return f"Error: {result['error']}"
|
|
675
|
+
return "Filled selection"
|
|
676
|
+
except KritaError as exc:
|
|
677
|
+
return _format_error(exc)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
@mcp.tool()
|
|
681
|
+
def krita_deselect() -> str:
|
|
682
|
+
"""Remove the current selection."""
|
|
683
|
+
try:
|
|
684
|
+
client = _get_client()
|
|
685
|
+
result = client.deselect()
|
|
686
|
+
if "error" in result:
|
|
687
|
+
return f"Error: {result['error']}"
|
|
688
|
+
return "Deselected"
|
|
689
|
+
except KritaError as exc:
|
|
690
|
+
return _format_error(exc)
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
@mcp.tool()
|
|
694
|
+
def krita_select_by_color(
|
|
695
|
+
x: int | None = None,
|
|
696
|
+
y: int | None = None,
|
|
697
|
+
tolerance: float = 0.1,
|
|
698
|
+
contiguous: bool = True,
|
|
699
|
+
) -> str:
|
|
700
|
+
"""Select pixels by color similarity.
|
|
701
|
+
|
|
702
|
+
Use x,y for magic wand (contiguous region from point).
|
|
703
|
+
Omit x,y for global color selection across entire canvas.
|
|
704
|
+
|
|
705
|
+
Args:
|
|
706
|
+
x: X coordinate for magic wand (None for global).
|
|
707
|
+
y: Y coordinate for magic wand (None for global).
|
|
708
|
+
tolerance: Color tolerance 0.0-1.0 (default 0.1).
|
|
709
|
+
contiguous: True for magic wand, False for global (default True).
|
|
710
|
+
"""
|
|
711
|
+
try:
|
|
712
|
+
client = _get_client()
|
|
713
|
+
result = client.select_by_color(x=x, y=y, tolerance=tolerance, contiguous=contiguous)
|
|
714
|
+
if "error" in result:
|
|
715
|
+
return f"Error: {result['error']}"
|
|
716
|
+
method = "Magic wand" if contiguous else "Global"
|
|
717
|
+
count = result.get("selected_count", 0)
|
|
718
|
+
return f"{method} color selection: {count} pixels (tolerance={tolerance})"
|
|
719
|
+
except KritaError as exc:
|
|
720
|
+
return _format_error(exc)
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
@mcp.tool()
|
|
724
|
+
def krita_select_by_alpha(
|
|
725
|
+
min_alpha: int = 1,
|
|
726
|
+
max_alpha: int = 255,
|
|
727
|
+
) -> str:
|
|
728
|
+
"""Select pixels by alpha value range.
|
|
729
|
+
|
|
730
|
+
Args:
|
|
731
|
+
min_alpha: Minimum alpha value 0-255 (default 1).
|
|
732
|
+
max_alpha: Maximum alpha value 0-255 (default 255).
|
|
733
|
+
"""
|
|
734
|
+
try:
|
|
735
|
+
client = _get_client()
|
|
736
|
+
result = client.select_by_alpha(min_alpha=min_alpha, max_alpha=max_alpha)
|
|
737
|
+
if "error" in result:
|
|
738
|
+
return f"Error: {result['error']}"
|
|
739
|
+
count = result.get("selected_count", 0)
|
|
740
|
+
return f"Alpha selection: {count} pixels (alpha={min_alpha}-{max_alpha})"
|
|
741
|
+
except KritaError as exc:
|
|
742
|
+
return _format_error(exc)
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
@mcp.tool()
|
|
746
|
+
def krita_get_capabilities() -> str:
|
|
747
|
+
"""Get detected API capabilities from the Krita plugin."""
|
|
748
|
+
try:
|
|
749
|
+
client = _get_client()
|
|
750
|
+
result = client.get_capabilities()
|
|
751
|
+
if "error" in result:
|
|
752
|
+
return f"Error: {result['error']}"
|
|
753
|
+
available_raw = result.get("selection_tools", [])
|
|
754
|
+
if not isinstance(available_raw, list):
|
|
755
|
+
available_raw = []
|
|
756
|
+
available = [str(t) for t in available_raw]
|
|
757
|
+
if available:
|
|
758
|
+
return f"Available selection tools: {', '.join(available)}"
|
|
759
|
+
return "No selection tools detected in this Krita version"
|
|
760
|
+
except KritaError as exc:
|
|
761
|
+
return _format_error(exc)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
@mcp.tool()
|
|
765
|
+
def krita_transform_selection(
|
|
766
|
+
dx: int = 0,
|
|
767
|
+
dy: int = 0,
|
|
768
|
+
angle: float = 0.0,
|
|
769
|
+
scale_x: float = 1.0,
|
|
770
|
+
scale_y: float = 1.0,
|
|
771
|
+
) -> str:
|
|
772
|
+
"""Transform the current selection (move, rotate, scale).
|
|
773
|
+
|
|
774
|
+
Args:
|
|
775
|
+
dx: Horizontal offset in pixels
|
|
776
|
+
dy: Vertical offset in pixels
|
|
777
|
+
angle: Rotation angle in degrees
|
|
778
|
+
scale_x: Horizontal scale factor
|
|
779
|
+
scale_y: Vertical scale factor
|
|
780
|
+
"""
|
|
781
|
+
try:
|
|
782
|
+
client = _get_client()
|
|
783
|
+
result = client.transform_selection(dx=dx, dy=dy, angle=angle, scale_x=scale_x, scale_y=scale_y)
|
|
784
|
+
if "error" in result:
|
|
785
|
+
return f"Error: {result['error']}"
|
|
786
|
+
return f"Transformed selection (dx={dx}, dy={dy}, angle={angle}°)"
|
|
787
|
+
except KritaError as exc:
|
|
788
|
+
return _format_error(exc)
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
@mcp.tool()
|
|
792
|
+
def krita_grow_selection(pixels: int) -> str:
|
|
793
|
+
"""Grow the current selection outward by N pixels."""
|
|
794
|
+
try:
|
|
795
|
+
client = _get_client()
|
|
796
|
+
result = client.grow_selection(pixels)
|
|
797
|
+
if "error" in result:
|
|
798
|
+
return f"Error: {result['error']}"
|
|
799
|
+
return f"Grew selection by {pixels}px"
|
|
800
|
+
except KritaError as exc:
|
|
801
|
+
return _format_error(exc)
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
@mcp.tool()
|
|
805
|
+
def krita_shrink_selection(pixels: int) -> str:
|
|
806
|
+
"""Shrink the current selection inward by N pixels."""
|
|
807
|
+
try:
|
|
808
|
+
client = _get_client()
|
|
809
|
+
result = client.shrink_selection(pixels)
|
|
810
|
+
if "error" in result:
|
|
811
|
+
return f"Error: {result['error']}"
|
|
812
|
+
return f"Shrunk selection by {pixels}px"
|
|
813
|
+
except KritaError as exc:
|
|
814
|
+
return _format_error(exc)
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
@mcp.tool()
|
|
818
|
+
def krita_border_selection(pixels: int) -> str:
|
|
819
|
+
"""Create a border selection around the current selection."""
|
|
820
|
+
try:
|
|
821
|
+
client = _get_client()
|
|
822
|
+
result = client.border_selection(pixels)
|
|
823
|
+
if "error" in result:
|
|
824
|
+
return f"Error: {result['error']}"
|
|
825
|
+
return f"Created {pixels}px border around selection"
|
|
826
|
+
except KritaError as exc:
|
|
827
|
+
return _format_error(exc)
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
@mcp.tool()
|
|
831
|
+
def krita_save_selection(path: str) -> str:
|
|
832
|
+
"""Save the current selection as a PNG mask image (white=selected, black=unselected)."""
|
|
833
|
+
try:
|
|
834
|
+
client = _get_client()
|
|
835
|
+
result = client.save_selection(path=path)
|
|
836
|
+
if "error" in result:
|
|
837
|
+
return f"Error: {result['error']}"
|
|
838
|
+
return f"Saved selection to {path}"
|
|
839
|
+
except KritaError as exc:
|
|
840
|
+
return _format_error(exc)
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
@mcp.tool()
|
|
844
|
+
def krita_load_selection(path: str) -> str:
|
|
845
|
+
"""Load a selection from a PNG mask image (white=selected, black=unselected)."""
|
|
846
|
+
try:
|
|
847
|
+
client = _get_client()
|
|
848
|
+
result = client.load_selection(path=path)
|
|
849
|
+
if "error" in result:
|
|
850
|
+
return f"Error: {result['error']}"
|
|
851
|
+
return f"Loaded selection from {path}"
|
|
852
|
+
except KritaError as exc:
|
|
853
|
+
return _format_error(exc)
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
@mcp.tool()
|
|
857
|
+
def krita_selection_stats() -> str:
|
|
858
|
+
"""Get statistics about the current selection (pixel count, centroid, bounding box, area %)."""
|
|
859
|
+
try:
|
|
860
|
+
client = _get_client()
|
|
861
|
+
result = client.selection_stats()
|
|
862
|
+
if "error" in result:
|
|
863
|
+
return f"Error: {result['error']}"
|
|
864
|
+
count = result.get("pixel_count", 0)
|
|
865
|
+
bbox_raw = result.get("bounding_box", {})
|
|
866
|
+
if not isinstance(bbox_raw, dict):
|
|
867
|
+
bbox_raw = {}
|
|
868
|
+
bbox = cast("dict[str, Any]", bbox_raw)
|
|
869
|
+
|
|
870
|
+
centroid_raw = result.get("centroid", {})
|
|
871
|
+
if not isinstance(centroid_raw, dict):
|
|
872
|
+
centroid_raw = {}
|
|
873
|
+
centroid = cast("dict[str, Any]", centroid_raw)
|
|
874
|
+
|
|
875
|
+
area_pct = result.get("area_percentage")
|
|
876
|
+
parts = [f"Pixel count: {count}"]
|
|
877
|
+
if bbox:
|
|
878
|
+
w = bbox.get("width", "?")
|
|
879
|
+
h = bbox.get("height", "?")
|
|
880
|
+
bx = bbox.get("x", "?")
|
|
881
|
+
by = bbox.get("y", "?")
|
|
882
|
+
parts.append(f"Bounding box: {w}x{h} at ({bx}, {by})")
|
|
883
|
+
if centroid:
|
|
884
|
+
cx = centroid.get("x", "?")
|
|
885
|
+
cy = centroid.get("y", "?")
|
|
886
|
+
parts.append(f"Centroid: ({cx}, {cy})")
|
|
887
|
+
if area_pct is not None:
|
|
888
|
+
# Handle possible float conversion for type safety
|
|
889
|
+
pct = float(cast("Any", area_pct))
|
|
890
|
+
parts.append(f"Area: {pct:.1f}% of canvas")
|
|
891
|
+
return "Selection stats: " + " | ".join(parts)
|
|
892
|
+
except KritaError as exc:
|
|
893
|
+
return _format_error(exc)
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
@mcp.tool()
|
|
897
|
+
def krita_save_selection_channel(name: str) -> str:
|
|
898
|
+
"""Save the current selection as a named channel within the document."""
|
|
899
|
+
try:
|
|
900
|
+
client = _get_client()
|
|
901
|
+
result = client.save_selection_channel(name=name)
|
|
902
|
+
if "error" in result:
|
|
903
|
+
return f"Error: {result['error']}"
|
|
904
|
+
return f"Saved selection channel '{name}'"
|
|
905
|
+
except KritaError as exc:
|
|
906
|
+
return _format_error(exc)
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
@mcp.tool()
|
|
910
|
+
def krita_load_selection_channel(name: str) -> str:
|
|
911
|
+
"""Load a named selection channel and restore it as the active selection."""
|
|
912
|
+
try:
|
|
913
|
+
client = _get_client()
|
|
914
|
+
result = client.load_selection_channel(name=name)
|
|
915
|
+
if "error" in result:
|
|
916
|
+
return f"Error: {result['error']}"
|
|
917
|
+
return f"Loaded selection channel '{name}'"
|
|
918
|
+
except KritaError as exc:
|
|
919
|
+
return _format_error(exc)
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
@mcp.tool()
|
|
923
|
+
def krita_list_selection_channels() -> str:
|
|
924
|
+
"""List all saved selection channels in the current document."""
|
|
925
|
+
try:
|
|
926
|
+
client = _get_client()
|
|
927
|
+
result = client.list_selection_channels()
|
|
928
|
+
if "error" in result:
|
|
929
|
+
return f"Error: {result['error']}"
|
|
930
|
+
channels_raw = result.get("channels", [])
|
|
931
|
+
if not isinstance(channels_raw, list):
|
|
932
|
+
channels_raw = []
|
|
933
|
+
channels = cast("list[dict[str, Any]]", channels_raw)
|
|
934
|
+
|
|
935
|
+
count = result.get("count", 0)
|
|
936
|
+
if count == 0:
|
|
937
|
+
return "No saved selection channels"
|
|
938
|
+
names = [str(ch.get("name", "?")) for ch in channels]
|
|
939
|
+
return f"Selection channels ({count}): {', '.join(names)}"
|
|
940
|
+
except KritaError as exc:
|
|
941
|
+
return _format_error(exc)
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
@mcp.tool()
|
|
945
|
+
def krita_security_status() -> str:
|
|
946
|
+
"""Get current security limits and usage from the Krita plugin."""
|
|
947
|
+
try:
|
|
948
|
+
client = _get_client()
|
|
949
|
+
result = client.get_security_status()
|
|
950
|
+
if "error" in result:
|
|
951
|
+
return f"Error: {result['error']}"
|
|
952
|
+
rl_raw = result.get("rate_limit", {})
|
|
953
|
+
if not isinstance(rl_raw, dict):
|
|
954
|
+
rl_raw = {}
|
|
955
|
+
rl = cast("dict[str, Any]", rl_raw)
|
|
956
|
+
|
|
957
|
+
payload_limit = result.get("payload_limit", 0)
|
|
958
|
+
if not isinstance(payload_limit, (int, float)):
|
|
959
|
+
payload_limit = 0
|
|
960
|
+
|
|
961
|
+
parts = [
|
|
962
|
+
f"Rate limit: {rl.get('current_usage', 0)}/{rl.get('max_commands_per_minute', '?')} per minute",
|
|
963
|
+
f"Payload limit: {float(payload_limit) / (1024 * 1024):.0f}MB",
|
|
964
|
+
f"Batch limit: {result.get('batch_size_limit', '?')} commands",
|
|
965
|
+
f"Max canvas: {result.get('max_canvas_dim', '?')}x{result.get('max_canvas_dim', '?')}",
|
|
966
|
+
]
|
|
967
|
+
return "Security status: " + " | ".join(parts)
|
|
968
|
+
except KritaError as exc:
|
|
969
|
+
return _format_error(exc)
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
@mcp.tool()
|
|
973
|
+
def krita_list_tools() -> str:
|
|
974
|
+
"""List all available Krita MCP tools with descriptions."""
|
|
975
|
+
tools = [
|
|
976
|
+
("krita_health", "Check Krita + plugin status"),
|
|
977
|
+
("krita_new_canvas", "Create canvas (width, height, bg color)"),
|
|
978
|
+
("krita_set_color", "Set foreground color (hex)"),
|
|
979
|
+
("krita_set_brush", "Set brush preset/size/opacity"),
|
|
980
|
+
("krita_stroke", "Paint stroke through [x, y] points"),
|
|
981
|
+
("krita_fill", "Fill circular area"),
|
|
982
|
+
("krita_draw_shape", "Draw rectangle/ellipse/line"),
|
|
983
|
+
("krita_get_canvas", "Export canvas to PNG"),
|
|
984
|
+
("krita_save", "Save canvas to file"),
|
|
985
|
+
("krita_undo", "Undo last action"),
|
|
986
|
+
("krita_redo", "Redo last action"),
|
|
987
|
+
("krita_clear", "Clear canvas"),
|
|
988
|
+
("krita_get_color_at", "Eyedropper - get color at pixel"),
|
|
989
|
+
("krita_list_brushes", "List brush presets"),
|
|
990
|
+
("krita_open_file", "Open existing file"),
|
|
991
|
+
("krita_batch", "Execute multiple commands sequentially"),
|
|
992
|
+
("krita_rollback", "Roll back a batch operation"),
|
|
993
|
+
("krita_select_rect", "Select a rectangular area"),
|
|
994
|
+
("krita_select_ellipse", "Select an elliptical area"),
|
|
995
|
+
("krita_select_polygon", "Select a polygonal area"),
|
|
996
|
+
("krita_selection_info", "Get current selection bounds"),
|
|
997
|
+
("krita_invert_selection", "Invert the current selection"),
|
|
998
|
+
("krita_clear_selection", "Clear selection contents"),
|
|
999
|
+
("krita_fill_selection", "Fill selection with foreground color"),
|
|
1000
|
+
("krita_deselect", "Remove current selection"),
|
|
1001
|
+
("krita_transform_selection", "Move/rotate/scale selection"),
|
|
1002
|
+
("krita_grow_selection", "Grow selection by N pixels"),
|
|
1003
|
+
("krita_shrink_selection", "Shrink selection by N pixels"),
|
|
1004
|
+
("krita_border_selection", "Create border around selection"),
|
|
1005
|
+
("krita_save_selection", "Save selection as PNG mask"),
|
|
1006
|
+
("krita_load_selection", "Load selection from PNG mask"),
|
|
1007
|
+
("krita_selection_stats", "Get selection statistics"),
|
|
1008
|
+
("krita_save_selection_channel", "Save selection as named channel"),
|
|
1009
|
+
("krita_load_selection_channel", "Load named selection channel"),
|
|
1010
|
+
("krita_list_selection_channels", "List saved selection channels"),
|
|
1011
|
+
("krita_select_by_color", "Select by color (magic wand/global)"),
|
|
1012
|
+
("krita_select_by_alpha", "Select by alpha range"),
|
|
1013
|
+
("krita_get_capabilities", "Get detected API capabilities"),
|
|
1014
|
+
("krita_security_status", "Get security limits and usage"),
|
|
1015
|
+
("krita_list_tools", "List all available MCP tools"),
|
|
1016
|
+
]
|
|
1017
|
+
lines = [f"- **{name}**: {desc}" for name, desc in tools]
|
|
1018
|
+
return f"Available Krita MCP tools ({len(tools)} total):\n" + "\n".join(lines)
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
if __name__ == "__main__": # pragma: no cover
|
|
1022
|
+
mcp.run()
|