everypixel-cli 0.1.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.
@@ -0,0 +1,828 @@
1
+ """MCP tools backed by the transport-independent application services."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Callable, Mapping
7
+ from contextvars import ContextVar
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from threading import Event
11
+ from typing import Any
12
+
13
+ import anyio
14
+ from mcp.server import Server
15
+ from mcp.server.stdio import stdio_server
16
+ from mcp.types import (
17
+ CallToolRequestParams,
18
+ CallToolResult,
19
+ ListToolsResult,
20
+ TextContent,
21
+ Tool,
22
+ ToolAnnotations,
23
+ )
24
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
25
+
26
+ from . import __version__
27
+ from .application import (
28
+ ApplicationServices,
29
+ ExecutionOptions,
30
+ OperationResult,
31
+ )
32
+ from .application.models import OperationCancelled
33
+ from .application.serialization import serialize_operation_result
34
+ from .client import APIClient
35
+ from .config import resolved_settings
36
+ from .errors import (
37
+ ValidationCLIError,
38
+ format_validation_errors,
39
+ normalize_exception,
40
+ )
41
+ from .schemas import (
42
+ ImageEditMedia,
43
+ ImageEditModel,
44
+ ImageGenerateModel,
45
+ ImageResolution,
46
+ ImageSize,
47
+ ImageStyle,
48
+ LipsyncImageModel,
49
+ LipsyncImageResolution,
50
+ LipsyncVideoResolution,
51
+ PublicFigureThreshold,
52
+ VideoEditAspectRatio,
53
+ VideoEditDuration,
54
+ VideoEditModel,
55
+ VideoEditResolution,
56
+ VideoGenerateAspectRatio,
57
+ VideoGenerateDuration,
58
+ VideoGenerateModel,
59
+ VideoGenerateResolution,
60
+ VideoUpscaleResolution,
61
+ )
62
+
63
+
64
+ ServiceFactory = Callable[[], ApplicationServices]
65
+ _CURRENT_CANCEL_EVENT: ContextVar[Event | None] = ContextVar(
66
+ "everypixel_mcp_cancellation",
67
+ default=None,
68
+ )
69
+
70
+ READ_ONLY = ToolAnnotations(
71
+ read_only_hint=True,
72
+ idempotent_hint=True,
73
+ open_world_hint=True,
74
+ )
75
+ LOCAL_WRITE = ToolAnnotations(
76
+ read_only_hint=False,
77
+ destructive_hint=True,
78
+ idempotent_hint=False,
79
+ open_world_hint=True,
80
+ )
81
+
82
+
83
+ class MCPExecutionOptions(BaseModel):
84
+ """Execution controls shared by asynchronous Everypixel tools."""
85
+
86
+ model_config = ConfigDict(extra="forbid")
87
+
88
+ wait: bool = Field(
89
+ default=True,
90
+ description=(
91
+ "Poll until the asynchronous task reaches SUCCESS; set false to "
92
+ "return the created task immediately."
93
+ ),
94
+ )
95
+ download_directory: str | None = Field(
96
+ default=None,
97
+ description="Local directory for completed result files; implies waiting.",
98
+ )
99
+ timeout: float = Field(
100
+ default=300.0,
101
+ gt=0,
102
+ description="Maximum task wait time in seconds.",
103
+ )
104
+ poll_interval: float = Field(
105
+ default=2.0,
106
+ gt=0,
107
+ description="Delay between task status requests in seconds.",
108
+ )
109
+
110
+
111
+ def _execution(
112
+ value: MCPExecutionOptions | None,
113
+ *,
114
+ force_wait: bool | None = None,
115
+ ) -> ExecutionOptions:
116
+ options = value or MCPExecutionOptions()
117
+ return ExecutionOptions(
118
+ wait=options.wait if force_wait is None else force_wait,
119
+ download_directory=(
120
+ Path(options.download_directory) if options.download_directory else None
121
+ ),
122
+ timeout=options.timeout,
123
+ poll_interval=options.poll_interval,
124
+ cancel_event=_CURRENT_CANCEL_EVENT.get(),
125
+ )
126
+
127
+
128
+ def _tool_result(payload: Any, *, is_error: bool = False) -> CallToolResult:
129
+ """Build one text and structured MCP result from a JSON-compatible value."""
130
+
131
+ structured = dict(payload) if isinstance(payload, Mapping) else {"result": payload}
132
+ return CallToolResult(
133
+ content=[
134
+ TextContent(
135
+ text=json.dumps(payload, ensure_ascii=False, default=str),
136
+ )
137
+ ],
138
+ structured_content=structured,
139
+ is_error=is_error,
140
+ )
141
+
142
+
143
+ def _invoke(action: Callable[[], Any]) -> CallToolResult:
144
+ """Execute a service action through the MCP success/error boundary."""
145
+
146
+ try:
147
+ value = action()
148
+ if isinstance(value, OperationResult):
149
+ value = serialize_operation_result(value)
150
+ return _tool_result(value)
151
+ except OperationCancelled:
152
+ raise
153
+ except Exception as exc: # The MCP boundary must never leak tracebacks or secrets.
154
+ return _tool_result(normalize_exception(exc).to_payload(), is_error=True)
155
+
156
+
157
+ class MCPToolInput(BaseModel):
158
+ """Strict base model shared by all public MCP tool inputs."""
159
+
160
+ model_config = ConfigDict(extra="forbid")
161
+
162
+
163
+ class ImageGenerateInput(MCPToolInput):
164
+ prompt: str
165
+ model: ImageGenerateModel = "zimage"
166
+ image_size: ImageSize = "square"
167
+ style: ImageStyle | None = None
168
+ image: str | None = None
169
+ resolution: ImageResolution | None = None
170
+ seed: int = -1
171
+ callback_url: str | None = None
172
+ execution: MCPExecutionOptions | None = None
173
+
174
+
175
+ class ImageEditInput(MCPToolInput):
176
+ prompt: str
177
+ images: ImageEditMedia
178
+ model: ImageEditModel = "flux2"
179
+ image_size: ImageSize | None = None
180
+ resolution: ImageResolution | None = None
181
+ seed: int = -1
182
+ callback_url: str | None = None
183
+ execution: MCPExecutionOptions | None = None
184
+
185
+
186
+ class ImageUpscaleInput(MCPToolInput):
187
+ image: str | None = None
188
+ task_id: str | None = None
189
+ model: str = "seedvr2"
190
+ callback_url: str | None = None
191
+ execution: MCPExecutionOptions | None = None
192
+
193
+
194
+ class ImageAnglesInput(MCPToolInput):
195
+ image: str
196
+ azimuth: str = "front"
197
+ elevation: str = "eye_level"
198
+ distance: str = "medium"
199
+ prompt: str | None = None
200
+ execution: MCPExecutionOptions | None = None
201
+
202
+
203
+ class ImageColorsInput(MCPToolInput):
204
+ image: str
205
+ reference: str
206
+ execution: MCPExecutionOptions | None = None
207
+
208
+
209
+ class VideoGenerateInput(MCPToolInput):
210
+ prompt: str
211
+ model: VideoGenerateModel = "ltx23"
212
+ duration: VideoGenerateDuration | None = None
213
+ resolution: VideoGenerateResolution | None = None
214
+ aspect_ratio: VideoGenerateAspectRatio | None = "16:9"
215
+ lora_high_url: str | None = None
216
+ lora_low_url: str | None = None
217
+ reference_images: list[str] | None = None
218
+ reference_videos: list[str] | None = None
219
+ image: str | None = None
220
+ last_image: str | None = None
221
+ seed: int | None = None
222
+ generate_audio: bool | None = None
223
+ callback_url: str | None = None
224
+ execution: MCPExecutionOptions | None = None
225
+
226
+
227
+ class VideoEditInput(MCPToolInput):
228
+ prompt: str | None = None
229
+ model: VideoEditModel = "seedance2"
230
+ images: list[str] | None = None
231
+ video: str | None = None
232
+ audio: str | None = None
233
+ duration: VideoEditDuration | None = None
234
+ resolution: VideoEditResolution | None = None
235
+ aspect_ratio: VideoEditAspectRatio | None = None
236
+ seed: int | None = None
237
+ generate_audio: bool | None = None
238
+ callback_url: str | None = None
239
+ keyframes: list[dict[str, Any]] | None = None
240
+ public_figure_threshold: PublicFigureThreshold | None = None
241
+ execution: MCPExecutionOptions | None = None
242
+
243
+
244
+ class VideoUpscaleInput(MCPToolInput):
245
+ video: str | None = None
246
+ task_id: str | None = None
247
+ resolution: VideoUpscaleResolution = "1080p"
248
+ execution: MCPExecutionOptions | None = None
249
+
250
+
251
+ class LipsyncVideoInput(MCPToolInput):
252
+ video: str
253
+ audio: str
254
+ resolution: LipsyncVideoResolution = "480p"
255
+ prompt: str | None = None
256
+ seed: int = -1
257
+ callback_url: str | None = None
258
+ execution: MCPExecutionOptions | None = None
259
+
260
+
261
+ class LipsyncImageInput(MCPToolInput):
262
+ image: str
263
+ audio: str
264
+ model: LipsyncImageModel = "inftalk"
265
+ resolution: LipsyncImageResolution = "480p"
266
+ prompt: str | None = None
267
+ seed: int = -1
268
+ callback_url: str | None = None
269
+ execution: MCPExecutionOptions | None = None
270
+
271
+
272
+ class AudioTranscribeInput(MCPToolInput):
273
+ audio: str
274
+ language: str = "auto"
275
+ hints: str = ""
276
+ denoise: bool = True
277
+ execution: MCPExecutionOptions | None = None
278
+
279
+
280
+ class TTSCreateInput(MCPToolInput):
281
+ text: str | None = None
282
+ text_file: str | None = None
283
+ speaker: str = "Ryan"
284
+ style: str = "Auto"
285
+ language: str = "Auto"
286
+ prompt: str = ""
287
+ seed: int = -1
288
+ execution: MCPExecutionOptions | None = None
289
+
290
+
291
+ class TTSCloneInput(MCPToolInput):
292
+ audio: str
293
+ text: str | None = None
294
+ text_file: str | None = None
295
+ language: str = "Auto"
296
+ seed: int = -1
297
+ execution: MCPExecutionOptions | None = None
298
+
299
+
300
+ class TTSVoiceInput(MCPToolInput):
301
+ text: str | None = None
302
+ text_file: str | None = None
303
+ character: str = "Female"
304
+ style: str = "Auto"
305
+ language: str = "Auto"
306
+ prompt: str = ""
307
+ seed: int = -1
308
+ execution: MCPExecutionOptions | None = None
309
+
310
+
311
+ class TaskStatusInput(MCPToolInput):
312
+ task_id: str
313
+
314
+
315
+ class TaskWaitInput(MCPToolInput):
316
+ task_id: str
317
+ execution: MCPExecutionOptions | None = None
318
+
319
+
320
+ class KeywordsInput(MCPToolInput):
321
+ image: str
322
+ lang: str = "en"
323
+ num_keywords: int | None = None
324
+ colors: bool = False
325
+
326
+
327
+ class ImageInput(MCPToolInput):
328
+ image: str
329
+
330
+
331
+ class VideoInput(MCPToolInput):
332
+ video: str
333
+
334
+
335
+ class RunInput(MCPToolInput):
336
+ endpoint: str
337
+ payload: dict[str, Any]
338
+ method: str | None = None
339
+ dry_run: bool = False
340
+ help_schema: bool = False
341
+ execution: MCPExecutionOptions | None = None
342
+
343
+
344
+ class NoInput(MCPToolInput):
345
+ pass
346
+
347
+
348
+ @dataclass(frozen=True)
349
+ class ToolSpec:
350
+ """One MCP contract and its transport-independent application handler."""
351
+
352
+ name: str
353
+ description: str
354
+ input_model: type[MCPToolInput]
355
+ annotations: ToolAnnotations
356
+ handler: Callable[[Any], Any]
357
+
358
+ def describe(self) -> Tool:
359
+ return Tool(
360
+ name=self.name,
361
+ description=self.description,
362
+ input_schema=self.input_model.model_json_schema(by_alias=True),
363
+ annotations=self.annotations,
364
+ )
365
+
366
+
367
+ class ToolRegistry:
368
+ """List, validate, and invoke MCP tools through one explicit boundary."""
369
+
370
+ def __init__(self) -> None:
371
+ self._tools: dict[str, ToolSpec] = {}
372
+
373
+ def tool(
374
+ self,
375
+ input_model: type[MCPToolInput],
376
+ *,
377
+ annotations: ToolAnnotations,
378
+ ) -> Callable[[Callable[[Any], Any]], Callable[[Any], Any]]:
379
+ def register(handler: Callable[[Any], Any]) -> Callable[[Any], Any]:
380
+ self._tools[handler.__name__] = ToolSpec(
381
+ name=handler.__name__,
382
+ description=handler.__doc__ or "",
383
+ input_model=input_model,
384
+ annotations=annotations,
385
+ handler=handler,
386
+ )
387
+ return handler
388
+
389
+ return register
390
+
391
+ def list_tools(self) -> list[Tool]:
392
+ return [spec.describe() for spec in self._tools.values()]
393
+
394
+ async def call_tool(self, params: CallToolRequestParams) -> CallToolResult:
395
+ spec = self._tools.get(params.name)
396
+ if spec is None:
397
+ return self._validation_error()
398
+ try:
399
+ arguments = spec.input_model.model_validate(params.arguments or {})
400
+ except ValidationError as exc:
401
+ return self._validation_error(exc)
402
+ cancel_event = Event()
403
+
404
+ def invoke() -> CallToolResult:
405
+ context_token = _CURRENT_CANCEL_EVENT.set(cancel_event)
406
+ try:
407
+ if cancel_event.is_set():
408
+ raise OperationCancelled
409
+ result = _invoke(lambda: spec.handler(arguments))
410
+ if cancel_event.is_set():
411
+ raise OperationCancelled
412
+ return result
413
+ finally:
414
+ _CURRENT_CANCEL_EVENT.reset(context_token)
415
+
416
+ try:
417
+ return await anyio.to_thread.run_sync(
418
+ invoke,
419
+ abandon_on_cancel=True,
420
+ )
421
+ finally:
422
+ cancel_event.set()
423
+
424
+ @staticmethod
425
+ def _validation_error(exc: ValidationError | None = None) -> CallToolResult:
426
+ message = (
427
+ format_validation_errors(exc.errors())
428
+ if exc is not None
429
+ else "Invalid MCP tool arguments"
430
+ )
431
+ return _tool_result(
432
+ ValidationCLIError(message).to_payload(),
433
+ is_error=True,
434
+ )
435
+
436
+
437
+ def _configured_services_factory(
438
+ *,
439
+ profile: str | None = None,
440
+ dev: bool = False,
441
+ base_url: str | None = None,
442
+ ) -> tuple[ServiceFactory, bool]:
443
+ settings = resolved_settings(profile=profile, dev=dev, base_url=base_url)
444
+
445
+ client = APIClient(
446
+ base_url=settings["base_url"],
447
+ client_id=settings["client_id"],
448
+ client_secret=settings["client_secret"],
449
+ )
450
+ configured_services = ApplicationServices.with_client(client)
451
+
452
+ def services() -> ApplicationServices:
453
+ return configured_services
454
+
455
+ return services, settings["client_id"] is not None
456
+
457
+
458
+ def create_mcp_server(
459
+ services_factory: ServiceFactory | None = None,
460
+ *,
461
+ client_id_present: bool | None = None,
462
+ ) -> Server[Any]:
463
+ """Create an Everypixel MCP server with injectable application services."""
464
+
465
+ if services_factory is None:
466
+ services_factory, configured_client_id = _configured_services_factory()
467
+ if client_id_present is None:
468
+ client_id_present = configured_client_id
469
+ client_id_present = bool(client_id_present)
470
+
471
+ registry = ToolRegistry()
472
+
473
+ @registry.tool(ImageGenerateInput, annotations=LOCAL_WRITE)
474
+ def image_generate(arguments: ImageGenerateInput) -> Any:
475
+ """Generate an image from a prompt, optionally using a source image."""
476
+
477
+ return services_factory().execute_image_generate(
478
+ prompt=arguments.prompt,
479
+ model=arguments.model,
480
+ image_size=arguments.image_size,
481
+ style=arguments.style,
482
+ image=arguments.image,
483
+ resolution=arguments.resolution,
484
+ seed=arguments.seed,
485
+ callback_url=arguments.callback_url,
486
+ execution=_execution(arguments.execution),
487
+ )
488
+
489
+ @registry.tool(ImageEditInput, annotations=LOCAL_WRITE)
490
+ def image_edit(arguments: ImageEditInput) -> Any:
491
+ """Edit one or more images using a text instruction."""
492
+
493
+ return services_factory().execute_image_edit(
494
+ prompt=arguments.prompt,
495
+ images=arguments.images,
496
+ model=arguments.model,
497
+ image_size=arguments.image_size,
498
+ resolution=arguments.resolution,
499
+ seed=arguments.seed,
500
+ callback_url=arguments.callback_url,
501
+ execution=_execution(arguments.execution),
502
+ )
503
+
504
+ @registry.tool(ImageUpscaleInput, annotations=LOCAL_WRITE)
505
+ def image_upscale(arguments: ImageUpscaleInput) -> Any:
506
+ """Upscale an image supplied directly or by a completed task ID."""
507
+
508
+ return services_factory().execute_image_upscale(
509
+ image=arguments.image,
510
+ task_id=arguments.task_id,
511
+ model=arguments.model,
512
+ callback_url=arguments.callback_url,
513
+ execution=_execution(arguments.execution),
514
+ )
515
+
516
+ @registry.tool(ImageAnglesInput, annotations=LOCAL_WRITE)
517
+ def image_angles(arguments: ImageAnglesInput) -> Any:
518
+ """Render an image from a different camera angle and distance."""
519
+
520
+ return services_factory().execute_image_angles(
521
+ image=arguments.image,
522
+ azimuth=arguments.azimuth,
523
+ elevation=arguments.elevation,
524
+ distance=arguments.distance,
525
+ prompt=arguments.prompt,
526
+ execution=_execution(arguments.execution),
527
+ )
528
+
529
+ @registry.tool(ImageColorsInput, annotations=LOCAL_WRITE)
530
+ def image_colors(arguments: ImageColorsInput) -> Any:
531
+ """Transfer colors from a reference image to a source image."""
532
+
533
+ return services_factory().execute_image_colors(
534
+ image=arguments.image,
535
+ reference=arguments.reference,
536
+ execution=_execution(arguments.execution),
537
+ )
538
+
539
+ @registry.tool(VideoGenerateInput, annotations=LOCAL_WRITE)
540
+ def video_generate(arguments: VideoGenerateInput) -> Any:
541
+ """Generate video from text, an image, or first and last frame images."""
542
+
543
+ return services_factory().execute_video_generate(
544
+ prompt=arguments.prompt,
545
+ model=arguments.model,
546
+ duration=arguments.duration,
547
+ resolution=arguments.resolution,
548
+ aspect_ratio=arguments.aspect_ratio,
549
+ lora_high_url=arguments.lora_high_url,
550
+ lora_low_url=arguments.lora_low_url,
551
+ reference_images=arguments.reference_images,
552
+ reference_videos=arguments.reference_videos,
553
+ image=arguments.image,
554
+ last_image=arguments.last_image,
555
+ seed=arguments.seed,
556
+ generate_audio=arguments.generate_audio,
557
+ callback_url=arguments.callback_url,
558
+ execution=_execution(arguments.execution),
559
+ )
560
+
561
+ @registry.tool(VideoEditInput, annotations=LOCAL_WRITE)
562
+ def video_edit(arguments: VideoEditInput) -> Any:
563
+ """Edit a video with model-specific image, video, audio, or keyframe inputs."""
564
+
565
+ encoded_keyframes = [
566
+ json.dumps(keyframe, ensure_ascii=False)
567
+ for keyframe in arguments.keyframes or []
568
+ ]
569
+ return services_factory().execute_video_edit(
570
+ prompt=arguments.prompt,
571
+ model=arguments.model,
572
+ images=arguments.images or [],
573
+ video=arguments.video,
574
+ audio=arguments.audio,
575
+ duration=arguments.duration,
576
+ resolution=arguments.resolution,
577
+ aspect_ratio=arguments.aspect_ratio,
578
+ seed=arguments.seed,
579
+ generate_audio=arguments.generate_audio,
580
+ callback_url=arguments.callback_url,
581
+ keyframes=encoded_keyframes,
582
+ public_figure_threshold=arguments.public_figure_threshold,
583
+ execution=_execution(arguments.execution),
584
+ )
585
+
586
+ @registry.tool(VideoUpscaleInput, annotations=LOCAL_WRITE)
587
+ def video_upscale(arguments: VideoUpscaleInput) -> Any:
588
+ """Upscale a video supplied directly or by a completed task ID."""
589
+
590
+ return services_factory().execute_video_upscale(
591
+ video=arguments.video,
592
+ task_id=arguments.task_id,
593
+ resolution=arguments.resolution,
594
+ execution=_execution(arguments.execution),
595
+ )
596
+
597
+ @registry.tool(LipsyncVideoInput, annotations=LOCAL_WRITE)
598
+ def lipsync_video(arguments: LipsyncVideoInput) -> Any:
599
+ """Synchronize lips in a video to an audio track."""
600
+
601
+ return services_factory().execute_lipsync_video(
602
+ video=arguments.video,
603
+ audio=arguments.audio,
604
+ resolution=arguments.resolution,
605
+ prompt=arguments.prompt,
606
+ seed=arguments.seed,
607
+ callback_url=arguments.callback_url,
608
+ execution=_execution(arguments.execution),
609
+ )
610
+
611
+ @registry.tool(LipsyncImageInput, annotations=LOCAL_WRITE)
612
+ def lipsync_image(arguments: LipsyncImageInput) -> Any:
613
+ """Create a lipsync video from a still image and audio track."""
614
+
615
+ return services_factory().execute_lipsync_image(
616
+ image=arguments.image,
617
+ audio=arguments.audio,
618
+ model=arguments.model,
619
+ resolution=arguments.resolution,
620
+ prompt=arguments.prompt,
621
+ seed=arguments.seed,
622
+ callback_url=arguments.callback_url,
623
+ execution=_execution(arguments.execution),
624
+ )
625
+
626
+ @registry.tool(AudioTranscribeInput, annotations=LOCAL_WRITE)
627
+ def audio_transcribe(arguments: AudioTranscribeInput) -> Any:
628
+ """Transcribe speech from an audio URL, data URI, or local file."""
629
+
630
+ return services_factory().execute_audio_transcribe(
631
+ audio=arguments.audio,
632
+ language=arguments.language,
633
+ hints=arguments.hints,
634
+ denoise=arguments.denoise,
635
+ execution=_execution(arguments.execution),
636
+ )
637
+
638
+ @registry.tool(TTSCreateInput, annotations=LOCAL_WRITE)
639
+ def tts_create(arguments: TTSCreateInput) -> Any:
640
+ """Create speech from text using a selected speaker."""
641
+
642
+ return services_factory().execute_tts_create(
643
+ text=arguments.text,
644
+ text_file=Path(arguments.text_file) if arguments.text_file else None,
645
+ speaker=arguments.speaker,
646
+ style=arguments.style,
647
+ language=arguments.language,
648
+ prompt=arguments.prompt,
649
+ seed=arguments.seed,
650
+ execution=_execution(arguments.execution),
651
+ )
652
+
653
+ @registry.tool(TTSCloneInput, annotations=LOCAL_WRITE)
654
+ def tts_clone(arguments: TTSCloneInput) -> Any:
655
+ """Create speech using a cloned voice sample."""
656
+
657
+ return services_factory().execute_tts_clone(
658
+ audio=arguments.audio,
659
+ text=arguments.text,
660
+ text_file=Path(arguments.text_file) if arguments.text_file else None,
661
+ language=arguments.language,
662
+ seed=arguments.seed,
663
+ execution=_execution(arguments.execution),
664
+ )
665
+
666
+ @registry.tool(TTSVoiceInput, annotations=LOCAL_WRITE)
667
+ def tts_voice(arguments: TTSVoiceInput) -> Any:
668
+ """Create speech from text using a character voice."""
669
+
670
+ return services_factory().execute_tts_voice(
671
+ text=arguments.text,
672
+ text_file=Path(arguments.text_file) if arguments.text_file else None,
673
+ character=arguments.character,
674
+ style=arguments.style,
675
+ language=arguments.language,
676
+ prompt=arguments.prompt,
677
+ seed=arguments.seed,
678
+ execution=_execution(arguments.execution),
679
+ )
680
+
681
+ @registry.tool(TaskStatusInput, annotations=READ_ONLY)
682
+ def task_status(arguments: TaskStatusInput) -> Any:
683
+ """Fetch the current state of an asynchronous Everypixel task once."""
684
+
685
+ return services_factory().get_task_status(
686
+ task_id=arguments.task_id,
687
+ execution=ExecutionOptions(
688
+ cancel_event=_CURRENT_CANCEL_EVENT.get(),
689
+ ),
690
+ )
691
+
692
+ @registry.tool(TaskWaitInput, annotations=LOCAL_WRITE)
693
+ def task_wait(arguments: TaskWaitInput) -> Any:
694
+ """Poll a task until SUCCESS and optionally download its result files."""
695
+
696
+ return services_factory().wait_for_task(
697
+ task_id=arguments.task_id,
698
+ execution=_execution(
699
+ arguments.execution,
700
+ force_wait=True,
701
+ ),
702
+ )
703
+
704
+ @registry.tool(KeywordsInput, annotations=READ_ONLY)
705
+ def keywords(arguments: KeywordsInput) -> Any:
706
+ """Extract semantic keywords and optional colors from an image."""
707
+
708
+ return services_factory().execute_keywords(
709
+ image=arguments.image,
710
+ lang=arguments.lang,
711
+ num_keywords=arguments.num_keywords,
712
+ colors=arguments.colors,
713
+ )
714
+
715
+ @registry.tool(ImageInput, annotations=READ_ONLY)
716
+ def quality(arguments: ImageInput) -> Any:
717
+ """Score the technical quality of an image."""
718
+
719
+ return services_factory().execute_quality(image=arguments.image)
720
+
721
+ @registry.tool(ImageInput, annotations=READ_ONLY)
722
+ def quality_ugc(arguments: ImageInput) -> Any:
723
+ """Score the user-generated-content quality of an image."""
724
+
725
+ return services_factory().execute_quality_ugc(image=arguments.image)
726
+
727
+ @registry.tool(ImageInput, annotations=READ_ONLY)
728
+ def faces(arguments: ImageInput) -> Any:
729
+ """Detect faces and facial attributes in an image."""
730
+
731
+ return services_factory().execute_faces(image=arguments.image)
732
+
733
+ @registry.tool(ImageInput, annotations=READ_ONLY)
734
+ def captioning(arguments: ImageInput) -> Any:
735
+ """Generate a natural-language caption for an image."""
736
+
737
+ return services_factory().execute_captioning(image=arguments.image)
738
+
739
+ @registry.tool(VideoInput, annotations=READ_ONLY)
740
+ def video_keywords(arguments: VideoInput) -> Any:
741
+ """Extract semantic keywords from a video."""
742
+
743
+ return services_factory().execute_video_keywords(video=arguments.video)
744
+
745
+ @registry.tool(RunInput, annotations=LOCAL_WRITE)
746
+ def run(arguments: RunInput) -> Any:
747
+ """Run any Everypixel operation by OpenAPI name or direct /v1 path."""
748
+
749
+ return services_factory().execute_generic(
750
+ endpoint=arguments.endpoint,
751
+ payload=arguments.payload,
752
+ method=arguments.method,
753
+ execution=_execution(arguments.execution),
754
+ dry_run=arguments.dry_run,
755
+ help_schema=arguments.help_schema,
756
+ client_id_present=client_id_present,
757
+ )
758
+
759
+ @registry.tool(NoInput, annotations=READ_ONLY)
760
+ def auth_check(_arguments: NoInput) -> Any:
761
+ """Verify that the configured Everypixel credentials are accepted."""
762
+
763
+ return services_factory().check_auth()
764
+
765
+ @registry.tool(NoInput, annotations=LOCAL_WRITE)
766
+ def openapi(_arguments: NoInput) -> Any:
767
+ """Return the selected live, cached, or bundled Everypixel OpenAPI schema."""
768
+
769
+ return services_factory().openapi()
770
+
771
+ @registry.tool(NoInput, annotations=LOCAL_WRITE)
772
+ def openapi_refresh(_arguments: NoInput) -> Any:
773
+ """Refresh the local Everypixel OpenAPI cache from the live API."""
774
+
775
+ result, path = services_factory().refresh_openapi()
776
+ return {
777
+ "schema": serialize_operation_result(result),
778
+ "cache_path": str(path),
779
+ }
780
+
781
+ async def list_tools(_ctx: Any, _params: Any) -> ListToolsResult:
782
+ return ListToolsResult(tools=registry.list_tools())
783
+
784
+ async def call_tool(_ctx: Any, params: CallToolRequestParams) -> CallToolResult:
785
+ return await registry.call_tool(params)
786
+
787
+ return Server(
788
+ "everypixel",
789
+ title="Everypixel API",
790
+ description="Generate and analyze media through the Everypixel API.",
791
+ instructions=(
792
+ "Generation tools wait for the completed result by default. Pass "
793
+ "execution.wait=false to return the created task immediately, then "
794
+ "use the task status or task wait tool. "
795
+ "Media arguments accept HTTP(S) URLs, data URIs, and local file paths. "
796
+ "Use English for natural-language prompts sent to generation tools; "
797
+ "translate prompts when needed while preserving the user's intent and "
798
+ "any text that must appear verbatim in the generated result."
799
+ ),
800
+ version=__version__,
801
+ on_list_tools=list_tools,
802
+ on_call_tool=call_tool,
803
+ )
804
+
805
+
806
+ async def _serve_stdio(server: Server[Any]) -> None:
807
+ async with stdio_server() as (read_stream, write_stream):
808
+ await server.run(
809
+ read_stream,
810
+ write_stream,
811
+ server.create_initialization_options(),
812
+ )
813
+
814
+
815
+ def run_mcp_server(server: Server[Any]) -> None:
816
+ """Run an Everypixel MCP server using the official stdio transport."""
817
+
818
+ anyio.run(_serve_stdio, server)
819
+
820
+
821
+ def main() -> None:
822
+ """Run the configured server over stdio without importing the CLI layer."""
823
+
824
+ run_mcp_server(create_mcp_server())
825
+
826
+
827
+ if __name__ == "__main__":
828
+ main()