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_client/config.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Configuration for the Krita MCP client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ClientConfig(BaseSettings):
|
|
9
|
+
"""Configuration for the Krita MCP client.
|
|
10
|
+
|
|
11
|
+
All settings can be overridden via environment variables
|
|
12
|
+
with the ``KRITA_`` prefix (e.g. ``KRITA_URL``, ``KRITA_PORT``).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
model_config = SettingsConfigDict(
|
|
16
|
+
env_prefix="KRITA_",
|
|
17
|
+
env_file=".env",
|
|
18
|
+
env_file_encoding="utf-8",
|
|
19
|
+
extra="ignore",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
url: str = "http://localhost:5678"
|
|
23
|
+
"""Base URL of the Krita plugin HTTP server."""
|
|
24
|
+
|
|
25
|
+
port: int = 5678
|
|
26
|
+
"""Port the Krita plugin listens on (used for URL construction if url is default)."""
|
|
27
|
+
|
|
28
|
+
default_timeout: float = 30.0
|
|
29
|
+
"""Default timeout in seconds for most commands."""
|
|
30
|
+
|
|
31
|
+
health_timeout: float = 5.0
|
|
32
|
+
"""Timeout in seconds for health checks."""
|
|
33
|
+
|
|
34
|
+
export_timeout: float = 120.0
|
|
35
|
+
"""Timeout in seconds for canvas export and save operations."""
|
|
36
|
+
|
|
37
|
+
max_canvas_width: int = 8192
|
|
38
|
+
"""Maximum allowed canvas width to prevent OOM."""
|
|
39
|
+
|
|
40
|
+
max_canvas_height: int = 8192
|
|
41
|
+
"""Maximum allowed canvas height to prevent OOM."""
|
|
42
|
+
|
|
43
|
+
canvas_output_dir: str = "~/krita-mcp-output"
|
|
44
|
+
"""Default directory for canvas exports."""
|
|
45
|
+
|
|
46
|
+
max_commands_per_minute: int = 60
|
|
47
|
+
"""Maximum commands per minute before rate limiting kicks in."""
|
|
48
|
+
|
|
49
|
+
max_batch_size: int = 50
|
|
50
|
+
"""Maximum number of commands allowed in a single batch request."""
|
|
51
|
+
|
|
52
|
+
max_layers: int = 100
|
|
53
|
+
"""Maximum number of layers per document to prevent OOM."""
|
krita_client/models.py
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
"""Pydantic models for all Krita MCP commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Annotated, Any, Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field, field_validator
|
|
9
|
+
|
|
10
|
+
# -- Error models -------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ErrorCode(str, Enum):
|
|
14
|
+
"""Semantic error codes for plugin errors."""
|
|
15
|
+
|
|
16
|
+
NO_ACTIVE_DOCUMENT = "NO_ACTIVE_DOCUMENT"
|
|
17
|
+
NO_ACTIVE_LAYER = "NO_ACTIVE_LAYER"
|
|
18
|
+
NO_ACTIVE_VIEW = "NO_ACTIVE_VIEW"
|
|
19
|
+
INVALID_COLOR = "INVALID_COLOR"
|
|
20
|
+
CANVAS_TOO_LARGE = "CANVAS_TOO_LARGE"
|
|
21
|
+
PATH_TRAVERSAL_BLOCKED = "PATH_TRAVERSAL_BLOCKED"
|
|
22
|
+
FILE_NOT_FOUND = "FILE_NOT_FOUND"
|
|
23
|
+
PLUGIN_UNREACHABLE = "PLUGIN_UNREACHABLE"
|
|
24
|
+
COMMAND_TIMEOUT = "COMMAND_TIMEOUT"
|
|
25
|
+
INVALID_SHAPE = "INVALID_SHAPE"
|
|
26
|
+
LAYER_NOT_FOUND = "LAYER_NOT_FOUND"
|
|
27
|
+
BRUSH_NOT_FOUND = "BRUSH_NOT_FOUND"
|
|
28
|
+
INVALID_PARAMETERS = "INVALID_PARAMETERS"
|
|
29
|
+
UNKNOWN_ACTION = "UNKNOWN_ACTION"
|
|
30
|
+
INTERNAL_ERROR = "INTERNAL_ERROR"
|
|
31
|
+
INCOMPATIBLE_PROTOCOL = "INCOMPATIBLE_PROTOCOL"
|
|
32
|
+
RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"
|
|
33
|
+
BATCH_SIZE_EXCEEDED = "BATCH_SIZE_EXCEEDED"
|
|
34
|
+
LAYER_LIMIT_EXCEEDED = "LAYER_LIMIT_EXCEEDED"
|
|
35
|
+
ROLLBACK_NOT_POSSIBLE = "ROLLBACK_NOT_POSSIBLE"
|
|
36
|
+
BATCH_NOT_FOUND = "BATCH_NOT_FOUND"
|
|
37
|
+
PAYLOAD_TOO_LARGE = "PAYLOAD_TOO_LARGE"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class KritaErrorResponse(BaseModel):
|
|
41
|
+
"""Structured error response from the plugin."""
|
|
42
|
+
|
|
43
|
+
code: ErrorCode
|
|
44
|
+
message: str
|
|
45
|
+
recoverable: bool = False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# -- Command models -----------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class NewCanvasParams(BaseModel):
|
|
52
|
+
"""Parameters for creating a new canvas."""
|
|
53
|
+
|
|
54
|
+
width: Annotated[int, Field(ge=1, le=8192)] = 800
|
|
55
|
+
height: Annotated[int, Field(ge=1, le=8192)] = 600
|
|
56
|
+
name: str = "New Canvas"
|
|
57
|
+
background: Annotated[str, Field(pattern=r"^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$")] = "#1a1a2e"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SetColorParams(BaseModel):
|
|
61
|
+
"""Parameters for setting the foreground color."""
|
|
62
|
+
|
|
63
|
+
color: Annotated[str, Field(pattern=r"^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$")]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SetBrushParams(BaseModel):
|
|
67
|
+
"""Parameters for setting brush properties."""
|
|
68
|
+
|
|
69
|
+
preset: str | None = None
|
|
70
|
+
size: Annotated[int | None, Field(ge=1, le=5000)] = None
|
|
71
|
+
opacity: Annotated[float | None, Field(ge=0.0, le=1.0)] = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class StrokeParams(BaseModel):
|
|
75
|
+
"""Parameters for painting a stroke."""
|
|
76
|
+
|
|
77
|
+
points: Annotated[list[list[int]], Field(min_length=2)]
|
|
78
|
+
pressure: Annotated[float, Field(ge=0.0, le=1.0)] = 1.0
|
|
79
|
+
size: Annotated[int | None, Field(ge=1, le=5000)] = None
|
|
80
|
+
hardness: Annotated[float, Field(ge=0.0, le=1.0)] = 0.5
|
|
81
|
+
opacity: Annotated[float, Field(ge=0.0, le=1.0)] = 1.0
|
|
82
|
+
|
|
83
|
+
@field_validator("points")
|
|
84
|
+
@classmethod
|
|
85
|
+
def validate_points(cls, value: list[list[int]]) -> list[list[int]]:
|
|
86
|
+
for i, pt in enumerate(value):
|
|
87
|
+
if len(pt) != 2:
|
|
88
|
+
msg = f"Point {i} must have exactly 2 coordinates, got {len(pt)}."
|
|
89
|
+
raise ValueError(msg)
|
|
90
|
+
return value
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class FillParams(BaseModel):
|
|
94
|
+
"""Parameters for filling a circular area."""
|
|
95
|
+
|
|
96
|
+
x: int
|
|
97
|
+
y: int
|
|
98
|
+
radius: Annotated[int, Field(ge=1, le=5000)] = 50
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class DrawShapeParams(BaseModel):
|
|
102
|
+
"""Parameters for drawing a shape."""
|
|
103
|
+
|
|
104
|
+
shape: Literal["rectangle", "ellipse", "line"]
|
|
105
|
+
x: int
|
|
106
|
+
y: int
|
|
107
|
+
width: int = 100
|
|
108
|
+
height: int = 100
|
|
109
|
+
fill: bool = True
|
|
110
|
+
stroke: bool = False
|
|
111
|
+
x2: int | None = None
|
|
112
|
+
y2: int | None = None
|
|
113
|
+
line_width: int = 2
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class GetCanvasParams(BaseModel):
|
|
117
|
+
"""Parameters for exporting the canvas."""
|
|
118
|
+
|
|
119
|
+
filename: str = "canvas.png"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class UndoParams(BaseModel):
|
|
123
|
+
"""Parameters for undo (empty)."""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class RedoParams(BaseModel):
|
|
127
|
+
"""Parameters for redo (empty)."""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class ClearParams(BaseModel):
|
|
131
|
+
"""Parameters for clearing the canvas."""
|
|
132
|
+
|
|
133
|
+
color: Annotated[str, Field(pattern=r"^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$")] = "#1a1a2e"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class SaveParams(BaseModel):
|
|
137
|
+
"""Parameters for saving the canvas."""
|
|
138
|
+
|
|
139
|
+
path: str
|
|
140
|
+
|
|
141
|
+
@field_validator("path")
|
|
142
|
+
@classmethod
|
|
143
|
+
def validate_path(cls, value: str) -> str:
|
|
144
|
+
if not value or not value.strip():
|
|
145
|
+
msg = "Path cannot be empty."
|
|
146
|
+
raise ValueError(msg)
|
|
147
|
+
if ".." in value:
|
|
148
|
+
msg = "Path traversal is not allowed."
|
|
149
|
+
raise ValueError(msg)
|
|
150
|
+
return value.strip()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class GetColorAtParams(BaseModel):
|
|
154
|
+
"""Parameters for sampling a pixel color."""
|
|
155
|
+
|
|
156
|
+
x: int
|
|
157
|
+
y: int
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class ListBrushesParams(BaseModel):
|
|
161
|
+
"""Parameters for listing brush presets."""
|
|
162
|
+
|
|
163
|
+
filter: str = ""
|
|
164
|
+
limit: Annotated[int, Field(ge=1, le=500)] = 20
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class OpenFileParams(BaseModel):
|
|
168
|
+
"""Parameters for opening a file."""
|
|
169
|
+
|
|
170
|
+
path: str
|
|
171
|
+
|
|
172
|
+
@field_validator("path")
|
|
173
|
+
@classmethod
|
|
174
|
+
def validate_path(cls, value: str) -> str:
|
|
175
|
+
if not value or not value.strip():
|
|
176
|
+
msg = "Path cannot be empty."
|
|
177
|
+
raise ValueError(msg)
|
|
178
|
+
if ".." in value:
|
|
179
|
+
msg = "Path traversal is not allowed."
|
|
180
|
+
raise ValueError(msg)
|
|
181
|
+
return value.strip()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class CanvasInfoParams(BaseModel):
|
|
185
|
+
"""Parameters for getting canvas information."""
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class CurrentColorParams(BaseModel):
|
|
189
|
+
"""Parameters for getting the current color."""
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class CurrentBrushParams(BaseModel):
|
|
193
|
+
"""Parameters for getting the current brush."""
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class ListLayersParams(BaseModel):
|
|
197
|
+
"""Parameters for listing layers."""
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class CreateLayerParams(BaseModel):
|
|
201
|
+
"""Parameters for creating a new layer."""
|
|
202
|
+
|
|
203
|
+
name: str = "New Layer"
|
|
204
|
+
layer_type: str = "paintlayer"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class SelectLayerParams(BaseModel):
|
|
208
|
+
"""Parameters for selecting a layer by name."""
|
|
209
|
+
|
|
210
|
+
name: str
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class DeleteLayerParams(BaseModel):
|
|
214
|
+
"""Parameters for deleting a layer by name."""
|
|
215
|
+
|
|
216
|
+
name: str
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class RenameLayerParams(BaseModel):
|
|
220
|
+
"""Parameters for renaming a layer."""
|
|
221
|
+
|
|
222
|
+
old_name: str
|
|
223
|
+
new_name: str
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class SetLayerOpacityParams(BaseModel):
|
|
227
|
+
"""Parameters for setting layer opacity."""
|
|
228
|
+
|
|
229
|
+
name: str
|
|
230
|
+
opacity: Annotated[float, Field(ge=0.0, le=1.0)]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class SetLayerVisibilityParams(BaseModel):
|
|
234
|
+
"""Parameters for toggling layer visibility."""
|
|
235
|
+
|
|
236
|
+
name: str
|
|
237
|
+
visible: bool
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# -- Selection operations -----------------------------------------------------
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class SelectRectParams(BaseModel):
|
|
244
|
+
"""Parameters for selecting a rectangular area."""
|
|
245
|
+
|
|
246
|
+
x: int
|
|
247
|
+
y: int
|
|
248
|
+
width: Annotated[int, Field(ge=1, le=8192)]
|
|
249
|
+
height: Annotated[int, Field(ge=1, le=8192)]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class SelectEllipseParams(BaseModel):
|
|
253
|
+
"""Parameters for selecting an elliptical area."""
|
|
254
|
+
|
|
255
|
+
cx: int
|
|
256
|
+
cy: int
|
|
257
|
+
rx: Annotated[int, Field(ge=1, le=8192)]
|
|
258
|
+
ry: Annotated[int, Field(ge=1, le=8192)]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class SelectPolygonParams(BaseModel):
|
|
262
|
+
"""Parameters for selecting a polygonal area."""
|
|
263
|
+
|
|
264
|
+
points: Annotated[list[list[int]], Field(min_length=3)]
|
|
265
|
+
|
|
266
|
+
@field_validator("points")
|
|
267
|
+
@classmethod
|
|
268
|
+
def validate_points(cls, value: list[list[int]]) -> list[list[int]]:
|
|
269
|
+
for i, pt in enumerate(value):
|
|
270
|
+
if len(pt) != 2:
|
|
271
|
+
msg = f"Point {i} must have exactly 2 coordinates, got {len(pt)}."
|
|
272
|
+
raise ValueError(msg)
|
|
273
|
+
return value
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class SelectionInfoParams(BaseModel):
|
|
277
|
+
"""Parameters for querying current selection info (empty)."""
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class ClearSelectionParams(BaseModel):
|
|
281
|
+
"""Parameters for clearing the current selection (empty)."""
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class InvertSelectionParams(BaseModel):
|
|
285
|
+
"""Parameters for inverting the current selection (empty)."""
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class FillSelectionParams(BaseModel):
|
|
289
|
+
"""Parameters for filling the current selection (empty)."""
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class DeselectParams(BaseModel):
|
|
293
|
+
"""Parameters for deselecting (empty)."""
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# -- Selection advanced features (color & alpha) ------------------------------
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class SelectByColorParams(BaseModel):
|
|
300
|
+
"""Parameters for selecting by color (magic wand / global)."""
|
|
301
|
+
|
|
302
|
+
x: int | None = None
|
|
303
|
+
y: int | None = None
|
|
304
|
+
tolerance: Annotated[float, Field(ge=0.0, le=1.0)] = 0.1
|
|
305
|
+
contiguous: bool = True
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class SelectByAlphaParams(BaseModel):
|
|
309
|
+
"""Parameters for selecting by alpha range."""
|
|
310
|
+
|
|
311
|
+
min_alpha: Annotated[int, Field(ge=0, le=255)] = 1
|
|
312
|
+
max_alpha: Annotated[int, Field(ge=0, le=255)] = 255
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# -- Selection persistence & session management --------------------------------
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class SaveSelectionParams(BaseModel):
|
|
319
|
+
"""Parameters for saving selection to file."""
|
|
320
|
+
|
|
321
|
+
path: str
|
|
322
|
+
format: str = "png"
|
|
323
|
+
|
|
324
|
+
@field_validator("path")
|
|
325
|
+
@classmethod
|
|
326
|
+
def validate_path(cls, value: str) -> str:
|
|
327
|
+
if not value or not value.strip():
|
|
328
|
+
msg = "Path cannot be empty."
|
|
329
|
+
raise ValueError(msg)
|
|
330
|
+
return value.strip()
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class LoadSelectionParams(BaseModel):
|
|
334
|
+
"""Parameters for loading selection from file."""
|
|
335
|
+
|
|
336
|
+
path: str
|
|
337
|
+
|
|
338
|
+
@field_validator("path")
|
|
339
|
+
@classmethod
|
|
340
|
+
def validate_path(cls, value: str) -> str:
|
|
341
|
+
if not value or not value.strip():
|
|
342
|
+
msg = "Path cannot be empty."
|
|
343
|
+
raise ValueError(msg)
|
|
344
|
+
return value.strip()
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class SelectionChannelParams(BaseModel):
|
|
348
|
+
"""Parameters for selection channel operations."""
|
|
349
|
+
|
|
350
|
+
name: str
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class SelectionStatsParams(BaseModel):
|
|
354
|
+
"""Parameters for getting selection statistics (empty)."""
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# -- Selection transforms & modifiers -----------------------------------------
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class TransformSelectionParams(BaseModel):
|
|
361
|
+
"""Parameters for transforming the current selection."""
|
|
362
|
+
|
|
363
|
+
dx: int = 0
|
|
364
|
+
dy: int = 0
|
|
365
|
+
angle: float = 0.0
|
|
366
|
+
scale_x: Annotated[float, Field(gt=0.0)] = 1.0
|
|
367
|
+
scale_y: Annotated[float, Field(gt=0.0)] = 1.0
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class ModifySelectionParams(BaseModel):
|
|
371
|
+
"""Parameters for grow/shrink/border operations."""
|
|
372
|
+
|
|
373
|
+
pixels: Annotated[int, Field(ge=1, le=1000)]
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class CombineSelectionParams(BaseModel):
|
|
377
|
+
"""Parameters for selection combination operations."""
|
|
378
|
+
|
|
379
|
+
operation: Literal["union", "intersect", "subtract"]
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# -- Batch operations ---------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class BatchCommand(BaseModel):
|
|
386
|
+
"""A single command within a batch."""
|
|
387
|
+
|
|
388
|
+
action: str
|
|
389
|
+
params: dict[str, Any] = {}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
class BatchRequest(BaseModel):
|
|
393
|
+
"""Parameters for batch execution."""
|
|
394
|
+
|
|
395
|
+
commands: Annotated[list[BatchCommand], Field(min_length=1)]
|
|
396
|
+
stop_on_error: bool = False
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
BatchParams = BatchRequest
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class BatchCommandResult(BaseModel):
|
|
403
|
+
"""Result of a single command within a batch."""
|
|
404
|
+
|
|
405
|
+
action: str
|
|
406
|
+
status: Literal["ok", "error"]
|
|
407
|
+
result: dict[str, Any] | None = None
|
|
408
|
+
error: str | None = None
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class BatchResponse(BaseModel):
|
|
412
|
+
"""Response for a batch execution."""
|
|
413
|
+
|
|
414
|
+
status: Literal["ok", "error", "partial"]
|
|
415
|
+
results: list[BatchCommandResult]
|
|
416
|
+
count: int
|
|
417
|
+
batch_id: str | None = None
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class RollbackParams(BaseModel):
|
|
421
|
+
"""Parameters for rolling back a batch."""
|
|
422
|
+
|
|
423
|
+
batch_id: str
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class RollbackResponse(BaseModel):
|
|
427
|
+
"""Response for a rollback operation."""
|
|
428
|
+
|
|
429
|
+
status: Literal["ok", "error"]
|
|
430
|
+
message: str | None = None
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# -- History models -----------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class CommandHistoryRecord(BaseModel):
|
|
437
|
+
"""A single command execution record in the history log."""
|
|
438
|
+
|
|
439
|
+
action: str
|
|
440
|
+
params: dict[str, Any] = {}
|
|
441
|
+
timestamp: float
|
|
442
|
+
status: Literal["ok", "error"]
|
|
443
|
+
duration_ms: float
|
|
444
|
+
error: str | None = None
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
class GetCommandHistoryParams(BaseModel):
|
|
448
|
+
"""Parameters for getting command history."""
|
|
449
|
+
|
|
450
|
+
limit: Annotated[int, Field(ge=1, le=500)] = 20
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# -- Command registry ---------------------------------------------------------
|
|
454
|
+
|
|
455
|
+
COMMAND_MODELS: dict[str, type[BaseModel]] = {
|
|
456
|
+
"new_canvas": NewCanvasParams,
|
|
457
|
+
"set_color": SetColorParams,
|
|
458
|
+
"set_brush": SetBrushParams,
|
|
459
|
+
"stroke": StrokeParams,
|
|
460
|
+
"fill": FillParams,
|
|
461
|
+
"draw_shape": DrawShapeParams,
|
|
462
|
+
"get_canvas": GetCanvasParams,
|
|
463
|
+
"undo": UndoParams,
|
|
464
|
+
"redo": RedoParams,
|
|
465
|
+
"clear": ClearParams,
|
|
466
|
+
"save": SaveParams,
|
|
467
|
+
"get_color_at": GetColorAtParams,
|
|
468
|
+
"list_brushes": ListBrushesParams,
|
|
469
|
+
"open_file": OpenFileParams,
|
|
470
|
+
"batch": BatchRequest,
|
|
471
|
+
"rollback": RollbackParams,
|
|
472
|
+
"list_layers": ListLayersParams,
|
|
473
|
+
"create_layer": CreateLayerParams,
|
|
474
|
+
"select_layer": SelectLayerParams,
|
|
475
|
+
"delete_layer": DeleteLayerParams,
|
|
476
|
+
"rename_layer": RenameLayerParams,
|
|
477
|
+
"set_layer_opacity": SetLayerOpacityParams,
|
|
478
|
+
"set_layer_visibility": SetLayerVisibilityParams,
|
|
479
|
+
"get_canvas_info": CanvasInfoParams,
|
|
480
|
+
"get_current_color": CurrentColorParams,
|
|
481
|
+
"get_current_brush": CurrentBrushParams,
|
|
482
|
+
"select_rect": SelectRectParams,
|
|
483
|
+
"select_ellipse": SelectEllipseParams,
|
|
484
|
+
"select_polygon": SelectPolygonParams,
|
|
485
|
+
"selection_info": SelectionInfoParams,
|
|
486
|
+
"get_capabilities": UndoParams,
|
|
487
|
+
"get_security_status": UndoParams,
|
|
488
|
+
"transform_selection": TransformSelectionParams,
|
|
489
|
+
"grow_selection": ModifySelectionParams,
|
|
490
|
+
"shrink_selection": ModifySelectionParams,
|
|
491
|
+
"border_selection": ModifySelectionParams,
|
|
492
|
+
"combine_selections": CombineSelectionParams,
|
|
493
|
+
"clear_selection": ClearSelectionParams,
|
|
494
|
+
"invert_selection": InvertSelectionParams,
|
|
495
|
+
"fill_selection": FillSelectionParams,
|
|
496
|
+
"deselect": DeselectParams,
|
|
497
|
+
"select_by_color": SelectByColorParams,
|
|
498
|
+
"select_by_alpha": SelectByAlphaParams,
|
|
499
|
+
"save_selection": SaveSelectionParams,
|
|
500
|
+
"load_selection": LoadSelectionParams,
|
|
501
|
+
"selection_stats": SelectionStatsParams,
|
|
502
|
+
"save_selection_channel": SelectionChannelParams,
|
|
503
|
+
"load_selection_channel": SelectionChannelParams,
|
|
504
|
+
"list_selection_channels": SelectionStatsParams,
|
|
505
|
+
"delete_selection_channel": SelectionChannelParams,
|
|
506
|
+
"get_command_history": GetCommandHistoryParams,
|
|
507
|
+
}
|
krita_client/schema.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""OpenAPI schema generation from pydantic models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from krita_client.models import COMMAND_MODELS
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def generate_openapi_schema(
|
|
11
|
+
title: str = "Krita MCP Plugin API",
|
|
12
|
+
version: str = "0.1.0",
|
|
13
|
+
description: str = "HTTP API for the Krita MCP plugin. Send POST requests to / with JSON bodies.",
|
|
14
|
+
) -> dict[str, Any]:
|
|
15
|
+
"""Generate an OpenAPI 3.1 schema from the command pydantic models.
|
|
16
|
+
|
|
17
|
+
This schema documents the HTTP API that the Krita plugin exposes,
|
|
18
|
+
making it easy for third-party integrations to understand the
|
|
19
|
+
available commands and their parameters.
|
|
20
|
+
"""
|
|
21
|
+
paths: dict[str, Any] = {}
|
|
22
|
+
schemas: dict[str, Any] = {}
|
|
23
|
+
|
|
24
|
+
for action, model_cls in COMMAND_MODELS.items():
|
|
25
|
+
schema = model_cls.model_json_schema()
|
|
26
|
+
model_name = model_cls.__name__
|
|
27
|
+
schemas[model_name] = schema
|
|
28
|
+
|
|
29
|
+
paths[f"/commands/{action}"] = {
|
|
30
|
+
"post": {
|
|
31
|
+
"summary": f"Execute the {action} command",
|
|
32
|
+
"requestBody": {
|
|
33
|
+
"required": True,
|
|
34
|
+
"content": {
|
|
35
|
+
"application/json": {
|
|
36
|
+
"schema": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"action": {"type": "string", "const": action},
|
|
40
|
+
"params": {"$ref": f"#/components/schemas/{model_name}"},
|
|
41
|
+
},
|
|
42
|
+
"required": ["action"],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
"responses": {
|
|
48
|
+
"200": {
|
|
49
|
+
"description": "Command executed successfully",
|
|
50
|
+
"content": {
|
|
51
|
+
"application/json": {
|
|
52
|
+
"schema": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"properties": {
|
|
55
|
+
"status": {"type": "string"},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
"500": {
|
|
62
|
+
"description": "Command execution failed",
|
|
63
|
+
"content": {
|
|
64
|
+
"application/json": {
|
|
65
|
+
"schema": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"properties": {
|
|
68
|
+
"error": {"type": "string"},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
paths["/health"] = {
|
|
79
|
+
"get": {
|
|
80
|
+
"summary": "Health check",
|
|
81
|
+
"responses": {
|
|
82
|
+
"200": {
|
|
83
|
+
"description": "Plugin is running",
|
|
84
|
+
"content": {
|
|
85
|
+
"application/json": {
|
|
86
|
+
"schema": {
|
|
87
|
+
"type": "object",
|
|
88
|
+
"properties": {
|
|
89
|
+
"status": {"type": "string"},
|
|
90
|
+
"plugin": {"type": "string"},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
"openapi": "3.1.0",
|
|
102
|
+
"info": {
|
|
103
|
+
"title": title,
|
|
104
|
+
"version": version,
|
|
105
|
+
"description": description,
|
|
106
|
+
},
|
|
107
|
+
"paths": paths,
|
|
108
|
+
"components": {"schemas": schemas},
|
|
109
|
+
}
|
krita_mcp/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Krita MCP server — FastMCP interface for AI agents."""
|