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_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."""