dcc-mcp-gimp 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ __pycache__/
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ dist/
6
+ *.egg-info/
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ ## [0.2.0](https://github.com/dcc-mcp/dcc-mcp-gimp/compare/v0.1.0...v0.2.0) (2026-07-24)
4
+
5
+
6
+ ### Features
7
+
8
+ * add GIMP MCP adapter ([4fcda51](https://github.com/dcc-mcp/dcc-mcp-gimp/commit/4fcda51b29d551e59175a81fa4fafcea5d2e8252))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * keep persistent GIMP bridge process alive ([441aa0c](https://github.com/dcc-mcp/dcc-mcp-gimp/commit/441aa0c8d7a8c2bf3178457e931ec2835115061c))
14
+ * match GIMP plugin folder to module name ([63799cb](https://github.com/dcc-mcp/dcc-mcp-gimp/commit/63799cbf1c60e53c7c56367b01b54d6290af76fb))
15
+ * use GIMP persistent procedure callback signature ([9489a4f](https://github.com/dcc-mcp/dcc-mcp-gimp/commit/9489a4f452b5d0bd947a3108b761ba26b0256aff))
16
+ * verify GIMP AppImage checksum ([573314b](https://github.com/dcc-mcp/dcc-mcp-gimp/commit/573314bc3bf1daf6e1e10ea722d03f66927a2eaa))
17
+
18
+ ## 0.1.0
19
+
20
+ - Initial GIMP 3 session bridge and MCP adapter.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 loonghao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: dcc-mcp-gimp
3
+ Version: 0.2.0
4
+ Summary: GIMP 3 adapter for the DCC Model Context Protocol
5
+ Project-URL: Homepage, https://github.com/dcc-mcp/dcc-mcp-gimp
6
+ Project-URL: Repository, https://github.com/dcc-mcp/dcc-mcp-gimp
7
+ Project-URL: Issues, https://github.com/dcc-mcp/dcc-mcp-gimp/issues
8
+ Author-email: loonghao <hal.long@outlook.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: automation,dcc,gimp,image-editing,mcp
12
+ Requires-Python: >=3.9
13
+ Requires-Dist: dcc-mcp-core<1.0.0,>=0.19.38
14
+ Provides-Extra: dev
15
+ Requires-Dist: build>=1.2; extra == 'dev'
16
+ Requires-Dist: pytest>=8; extra == 'dev'
17
+ Requires-Dist: ruff>=0.8; extra == 'dev'
18
+ Requires-Dist: twine>=6; extra == 'dev'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # dcc-mcp-gimp
22
+
23
+ GIMP 3 adapter for the DCC Model Context Protocol ecosystem.
24
+
25
+ The adapter uses a small GIMP 3 Python plug-in and a loopback JSON-lines bridge.
26
+ The MCP server runs in the normal Python environment; GIMP API calls stay inside
27
+ the plug-in process. It does not expose arbitrary Python or Script-Fu execution.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install dcc-mcp-gimp
33
+ dcc-mcp-gimp-install
34
+ ```
35
+
36
+ Restart GIMP, run **Filters → Development → DCC-MCP GIMP Bridge**, then start:
37
+
38
+ ```bash
39
+ dcc-mcp-gimp
40
+ ```
41
+
42
+ The MCP endpoint defaults to `http://127.0.0.1:8767/mcp`; the plug-in bridge uses
43
+ `127.0.0.1:3848`. Override the latter with `DCC_MCP_GIMP_BRIDGE_PORT` before
44
+ starting both processes.
45
+
46
+ ## Current tools
47
+
48
+ - Check GIMP bridge status and version.
49
+ - List open images with dimensions.
50
+ - Inspect the active image.
51
+
52
+ The first release targets safe session discovery. Image mutation and export will
53
+ be added only through typed GIMP procedures, not arbitrary source evaluation.
54
+
55
+ ## Development
56
+
57
+ ```bash
58
+ python -m pip install -e ".[dev]"
59
+ python -m pytest
60
+ python -m ruff check src tests tools
61
+ python tools/lint_skills.py
62
+ python -m build
63
+ python -m twine check dist/*
64
+ ```
65
+
66
+ GIMP 3 plug-in API reference: https://developer.gimp.org/api/3.0/
@@ -0,0 +1,46 @@
1
+ # dcc-mcp-gimp
2
+
3
+ GIMP 3 adapter for the DCC Model Context Protocol ecosystem.
4
+
5
+ The adapter uses a small GIMP 3 Python plug-in and a loopback JSON-lines bridge.
6
+ The MCP server runs in the normal Python environment; GIMP API calls stay inside
7
+ the plug-in process. It does not expose arbitrary Python or Script-Fu execution.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install dcc-mcp-gimp
13
+ dcc-mcp-gimp-install
14
+ ```
15
+
16
+ Restart GIMP, run **Filters → Development → DCC-MCP GIMP Bridge**, then start:
17
+
18
+ ```bash
19
+ dcc-mcp-gimp
20
+ ```
21
+
22
+ The MCP endpoint defaults to `http://127.0.0.1:8767/mcp`; the plug-in bridge uses
23
+ `127.0.0.1:3848`. Override the latter with `DCC_MCP_GIMP_BRIDGE_PORT` before
24
+ starting both processes.
25
+
26
+ ## Current tools
27
+
28
+ - Check GIMP bridge status and version.
29
+ - List open images with dimensions.
30
+ - Inspect the active image.
31
+
32
+ The first release targets safe session discovery. Image mutation and export will
33
+ be added only through typed GIMP procedures, not arbitrary source evaluation.
34
+
35
+ ## Development
36
+
37
+ ```bash
38
+ python -m pip install -e ".[dev]"
39
+ python -m pytest
40
+ python -m ruff check src tests tools
41
+ python tools/lint_skills.py
42
+ python -m build
43
+ python -m twine check dist/*
44
+ ```
45
+
46
+ GIMP 3 plug-in API reference: https://developer.gimp.org/api/3.0/
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env python3
2
+ """GIMP 3 Python plug-in exposing a loopback JSON-lines bridge."""
3
+
4
+ import json
5
+ import os
6
+ import socketserver
7
+ import sys
8
+ import threading
9
+
10
+ import gi
11
+
12
+ gi.require_version("Gimp", "3.0")
13
+ from gi.repository import Gimp, GLib
14
+
15
+
16
+ PORT = int(os.environ.get("DCC_MCP_GIMP_BRIDGE_PORT", "3848"))
17
+
18
+
19
+ def _image_info(image):
20
+ return {
21
+ "id": int(image.get_id()),
22
+ "name": image.get_name(),
23
+ "width": image.get_width(),
24
+ "height": image.get_height(),
25
+ }
26
+
27
+
28
+ def _dispatch_main(method):
29
+ if method == "gimp.get_status":
30
+ return {"ready": True, "gimp_version": Gimp.version(), "bridge_port": PORT}
31
+ if method == "gimp.list_images":
32
+ return [_image_info(image) for image in Gimp.get_images()]
33
+ if method == "gimp.get_active_image":
34
+ image = Gimp.get_images()[0] if Gimp.get_images() else None
35
+ return _image_info(image) if image else None
36
+ if method == "gimp.ping":
37
+ return {"ready": True}
38
+ raise ValueError(f"Unsupported GIMP bridge method: {method}")
39
+
40
+
41
+ def _dispatch(method):
42
+ result = {}
43
+ completed = threading.Event()
44
+
45
+ def run_on_main():
46
+ try:
47
+ result["value"] = _dispatch_main(method)
48
+ except Exception as exc:
49
+ result["error"] = exc
50
+ finally:
51
+ completed.set()
52
+ return GLib.SOURCE_REMOVE
53
+
54
+ GLib.idle_add(run_on_main)
55
+ if not completed.wait(10):
56
+ raise TimeoutError("GIMP main thread did not answer within 10 seconds")
57
+ if "error" in result:
58
+ raise result["error"]
59
+ return result.get("value")
60
+
61
+
62
+ class _Handler(socketserver.StreamRequestHandler):
63
+ def handle(self):
64
+ line = self.rfile.readline().decode("utf-8")
65
+ if not line:
66
+ return
67
+ request = json.loads(line)
68
+ try:
69
+ result = _dispatch(request["method"])
70
+ response = {"jsonrpc": "2.0", "id": request.get("id"), "result": result}
71
+ except Exception as exc: # GIMP procedure errors must reach the MCP caller.
72
+ response = {"jsonrpc": "2.0", "id": request.get("id"), "error": str(exc)}
73
+ self.wfile.write((json.dumps(response) + "\n").encode("utf-8"))
74
+
75
+
76
+ class _Server(socketserver.ThreadingTCPServer):
77
+ allow_reuse_address = True
78
+ daemon_threads = True
79
+
80
+
81
+ class DccMcpGimp(Gimp.PlugIn):
82
+ def do_query_procedures(self):
83
+ return ["python-fu-dcc-mcp-gimp-bridge"]
84
+
85
+ def do_create_procedure(self, name):
86
+ procedure = Gimp.Procedure.new(
87
+ self, name, Gimp.PDBProcType.PERSISTENT, self._run, self, None
88
+ )
89
+ procedure.set_documentation(
90
+ "Start the DCC-MCP GIMP bridge",
91
+ "Starts a loopback bridge for dcc-mcp-gimp.",
92
+ "dcc-mcp-gimp",
93
+ )
94
+ return procedure
95
+
96
+ @staticmethod
97
+ def _run(procedure, run_mode, config, plugin):
98
+ server = _Server(("127.0.0.1", PORT), _Handler)
99
+ threading.Thread(target=server.serve_forever, daemon=True).start()
100
+ procedure.persistent_ready()
101
+ plugin.persistent_enable()
102
+ GLib.MainLoop().run()
103
+ return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, None)
104
+
105
+
106
+ Gimp.main(DccMcpGimp.__gtype__, sys.argv)
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.26"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dcc-mcp-gimp"
7
+ version = "0.2.0"
8
+ description = "GIMP 3 adapter for the DCC Model Context Protocol"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "loonghao", email = "hal.long@outlook.com" }]
13
+ keywords = ["gimp", "image-editing", "mcp", "dcc", "automation"]
14
+ dependencies = ["dcc-mcp-core>=0.19.38,<1.0.0"]
15
+
16
+ [project.optional-dependencies]
17
+ dev = ["build>=1.2", "pytest>=8", "ruff>=0.8", "twine>=6"]
18
+
19
+ [project.urls]
20
+ Homepage = "https://github.com/dcc-mcp/dcc-mcp-gimp"
21
+ Repository = "https://github.com/dcc-mcp/dcc-mcp-gimp"
22
+ Issues = "https://github.com/dcc-mcp/dcc-mcp-gimp/issues"
23
+
24
+ [project.scripts]
25
+ dcc-mcp-gimp = "dcc_mcp_gimp.server:main"
26
+ dcc-mcp-gimp-install = "dcc_mcp_gimp.install:main"
27
+
28
+ [project.entry-points."dcc_mcp.adapters"]
29
+ gimp = "dcc_mcp_gimp:GimpMcpServer"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/dcc_mcp_gimp"]
33
+ artifacts = ["src/dcc_mcp_gimp/skills/**"]
34
+
35
+ [tool.hatch.build.targets.wheel.force-include]
36
+ "bridge/gimp-plugin/dcc_mcp_gimp.py" = "dcc_mcp_gimp/gimp_plugin/dcc_mcp_gimp.py"
37
+
38
+ [tool.hatch.build.targets.sdist]
39
+ include = ["src/dcc_mcp_gimp", "bridge", "tests", "README.md", "LICENSE", "CHANGELOG.md", "tools"]
40
+
41
+ [tool.pytest.ini_options]
42
+ testpaths = ["tests"]
43
+ pythonpath = ["src", "."]
44
+
45
+ [tool.ruff]
46
+ target-version = "py39"
47
+ line-length = 100
48
+
49
+ [tool.ruff.lint]
50
+ select = ["E", "F", "I", "B"]
@@ -0,0 +1,3 @@
1
+ from .server import GimpMcpServer
2
+
3
+ __all__ = ["GimpMcpServer"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,51 @@
1
+ """Small JSON-lines client for the GIMP 3 plug-in bridge."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import socket
8
+ from typing import Any
9
+
10
+
11
+ class GimpBridgeError(RuntimeError):
12
+ """Raised when the GIMP plug-in bridge is unavailable or rejects a call."""
13
+
14
+
15
+ class GimpBridge:
16
+ def __init__(self, host: str = "127.0.0.1", port: int = 3848, timeout: float = 10.0) -> None:
17
+ self.host = host
18
+ self.port = port
19
+ self.timeout = timeout
20
+
21
+ @classmethod
22
+ def from_env(cls) -> "GimpBridge":
23
+ return cls(
24
+ host=os.environ.get("DCC_MCP_GIMP_BRIDGE_HOST", "127.0.0.1"),
25
+ port=int(os.environ.get("DCC_MCP_GIMP_BRIDGE_PORT", "3848")),
26
+ timeout=float(os.environ.get("DCC_MCP_GIMP_BRIDGE_TIMEOUT", "10")),
27
+ )
28
+
29
+ def call(self, method: str, **params: Any) -> Any:
30
+ request = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": params})
31
+ try:
32
+ with socket.create_connection(
33
+ (self.host, self.port), timeout=self.timeout
34
+ ) as connection:
35
+ connection.sendall((request + "\n").encode("utf-8"))
36
+ response = connection.makefile("r", encoding="utf-8").readline()
37
+ except OSError as exc:
38
+ raise GimpBridgeError(
39
+ f"GIMP bridge unavailable at {self.host}:{self.port}; "
40
+ "install and run the GIMP 3 plug-in"
41
+ ) from exc
42
+ if not response:
43
+ raise GimpBridgeError("GIMP bridge closed the connection without a response")
44
+ payload = json.loads(response)
45
+ if "error" in payload:
46
+ raise GimpBridgeError(str(payload["error"]))
47
+ return payload.get("result")
48
+
49
+
50
+ def get_bridge() -> GimpBridge:
51
+ return GimpBridge.from_env()
@@ -0,0 +1,35 @@
1
+ """Install the GIMP 3 Python plug-in into a user plug-in directory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import shutil
8
+ from pathlib import Path
9
+
10
+
11
+ def default_plugin_dir() -> Path:
12
+ if os.name == "nt":
13
+ root = Path(os.environ.get("APPDATA", Path.home() / "AppData/Roaming"))
14
+ return root / "GIMP/3.0/plug-ins"
15
+ return Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "GIMP/3.0/plug-ins"
16
+
17
+
18
+ def install(destination: Path | None = None) -> Path:
19
+ target = (destination or default_plugin_dir()).expanduser().resolve() / "dcc_mcp_gimp"
20
+ target.mkdir(parents=True, exist_ok=True)
21
+ source = Path(__file__).resolve().parent / "gimp_plugin" / "dcc_mcp_gimp.py"
22
+ if not source.is_file():
23
+ source = Path(__file__).resolve().parents[2] / "bridge" / "gimp-plugin" / "dcc_mcp_gimp.py"
24
+ if not source.is_file():
25
+ raise FileNotFoundError(f"Bundled GIMP plug-in not found: {source}")
26
+ shutil.copy2(source, target / source.name)
27
+ if os.name != "nt":
28
+ (target / source.name).chmod(0o755)
29
+ return target
30
+
31
+
32
+ def main() -> None:
33
+ parser = argparse.ArgumentParser()
34
+ parser.add_argument("--destination", type=Path)
35
+ print(install(parser.parse_args().destination))
@@ -0,0 +1,62 @@
1
+ """Standalone GIMP MCP server lifecycle."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import signal
7
+ import sys
8
+ import threading
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ from dcc_mcp_core import DccServerOptions
13
+ from dcc_mcp_core.server_base import DccServerBase
14
+
15
+ from .__version__ import __version__
16
+
17
+ DEFAULT_PORT = 8767
18
+ _server: Optional["GimpMcpServer"] = None
19
+
20
+
21
+ class GimpMcpServer(DccServerBase):
22
+ """GIMP 3 adapter using its Python plug-in bridge."""
23
+
24
+ def __init__(self, port: int = DEFAULT_PORT) -> None:
25
+ os.environ.setdefault("DCC_MCP_PYTHON_EXECUTABLE", sys.executable)
26
+ options = DccServerOptions.from_env(
27
+ "gimp",
28
+ Path(__file__).resolve().parent / "skills",
29
+ port=port,
30
+ server_name="dcc-mcp-gimp",
31
+ server_version=__version__,
32
+ )
33
+ super().__init__(options=options)
34
+
35
+
36
+ def start_server(port: Optional[int] = None) -> GimpMcpServer:
37
+ global _server
38
+ if _server is None or not _server.is_running:
39
+ selected_port = port or int(os.environ.get("DCC_MCP_GIMP_PORT", DEFAULT_PORT))
40
+ _server = GimpMcpServer(selected_port)
41
+ _server.register_builtin_actions()
42
+ _server.start()
43
+ return _server
44
+
45
+
46
+ def stop_server() -> None:
47
+ global _server
48
+ if _server is not None:
49
+ _server.stop()
50
+ _server = None
51
+
52
+
53
+ def main() -> None:
54
+ stopped = threading.Event()
55
+ signal.signal(signal.SIGINT, lambda *_: stopped.set())
56
+ if hasattr(signal, "SIGTERM"):
57
+ signal.signal(signal.SIGTERM, lambda *_: stopped.set())
58
+ start_server()
59
+ try:
60
+ stopped.wait()
61
+ finally:
62
+ stop_server()
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: gimp-session
3
+ description: >-
4
+ Inspect the connected GIMP 3 session through the DCC-MCP Python plug-in
5
+ bridge. Use for session health, open images, and active image metadata.
6
+ license: MIT
7
+ compatibility: "GIMP 3.0+; dcc-mcp-core 0.19+"
8
+ allowed-tools: "python"
9
+ metadata:
10
+ dcc-mcp:
11
+ dcc: gimp
12
+ layer: domain
13
+ version: "0.1.0"
14
+ search-hint: "GIMP image editor session document active image layers"
15
+ tags: "gimp,image-editing,session"
16
+ tools: tools.yaml
17
+ depends: "dcc-diagnostics"
18
+ ---
19
+
20
+ # GIMP Session
21
+
22
+ Install and run the bundled GIMP 3 plug-in before using this skill. Calls use a
23
+ loopback JSON-lines bridge and never execute arbitrary GIMP/Python source.
@@ -0,0 +1,15 @@
1
+ from dcc_mcp_core.skill import run_main, skill_entry, skill_success
2
+
3
+ from dcc_mcp_gimp.bridge import get_bridge
4
+
5
+
6
+ @skill_entry
7
+ def main(**_kwargs):
8
+ return skill_success(
9
+ "Active GIMP image inspected.",
10
+ image=get_bridge().call("gimp.get_active_image"),
11
+ )
12
+
13
+
14
+ if __name__ == "__main__":
15
+ run_main(main)
@@ -0,0 +1,12 @@
1
+ from dcc_mcp_core.skill import run_main, skill_entry, skill_success
2
+
3
+ from dcc_mcp_gimp.bridge import get_bridge
4
+
5
+
6
+ @skill_entry
7
+ def main(**_kwargs):
8
+ return skill_success("GIMP bridge is ready.", **get_bridge().call("gimp.get_status"))
9
+
10
+
11
+ if __name__ == "__main__":
12
+ run_main(main)
@@ -0,0 +1,13 @@
1
+ from dcc_mcp_core.skill import run_main, skill_entry, skill_success
2
+
3
+ from dcc_mcp_gimp.bridge import get_bridge
4
+
5
+
6
+ @skill_entry
7
+ def main(**_kwargs):
8
+ images = get_bridge().call("gimp.list_images")
9
+ return skill_success(f"Found {len(images)} open image(s).", count=len(images), images=images)
10
+
11
+
12
+ if __name__ == "__main__":
13
+ run_main(main)
@@ -0,0 +1,34 @@
1
+ tools:
2
+ - name: get_status
3
+ description: Check the GIMP 3 plug-in bridge and return GIMP version and bridge metadata.
4
+ input_schema: {type: object, properties: {}, additionalProperties: false}
5
+ output_schema: {type: object}
6
+ read_only: true
7
+ destructive: false
8
+ idempotent: true
9
+ execution: sync
10
+ affinity: any
11
+ timeout_hint_secs: 30
12
+ source_file: scripts/get_status.py
13
+ - name: list_images
14
+ description: List open GIMP images with dimensions and names. Read-only.
15
+ input_schema: {type: object, properties: {}, additionalProperties: false}
16
+ output_schema: {type: object}
17
+ read_only: true
18
+ destructive: false
19
+ idempotent: true
20
+ execution: sync
21
+ affinity: any
22
+ timeout_hint_secs: 30
23
+ source_file: scripts/list_images.py
24
+ - name: get_active_image
25
+ description: Return metadata for the active GIMP image. Read-only.
26
+ input_schema: {type: object, properties: {}, additionalProperties: false}
27
+ output_schema: {type: object}
28
+ read_only: true
29
+ destructive: false
30
+ idempotent: true
31
+ execution: sync
32
+ affinity: any
33
+ timeout_hint_secs: 30
34
+ source_file: scripts/get_active_image.py
@@ -0,0 +1,67 @@
1
+ """Exercise the real GIMP plug-in bridge and MCP tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ import urllib.request
8
+
9
+ from dcc_mcp_gimp.bridge import GimpBridge
10
+ from dcc_mcp_gimp.server import GimpMcpServer
11
+
12
+
13
+ def post(url: str, method: str, params=None):
14
+ request = urllib.request.Request(
15
+ url,
16
+ data=json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}).encode(),
17
+ headers={
18
+ "Accept": "application/json, text/event-stream",
19
+ "Content-Type": "application/json",
20
+ },
21
+ )
22
+ with urllib.request.urlopen(request, timeout=30) as response:
23
+ return json.loads(response.read())
24
+
25
+
26
+ def call(url: str, name: str, arguments=None):
27
+ response = post(url, "tools/call", {"name": name, "arguments": arguments or {}})
28
+ result = response.get("result", {})
29
+ if response.get("error") or result.get("isError"):
30
+ raise RuntimeError(json.dumps(response))
31
+ structured = result.get("structuredContent")
32
+ if structured is not None:
33
+ return structured
34
+ return json.loads(result["content"][0]["text"])
35
+
36
+
37
+ def main() -> None:
38
+ bridge = GimpBridge.from_env()
39
+ deadline = time.monotonic() + 45
40
+ while time.monotonic() < deadline:
41
+ try:
42
+ status = bridge.call("gimp.get_status")
43
+ break
44
+ except Exception:
45
+ time.sleep(0.5)
46
+ else:
47
+ raise RuntimeError("GIMP plug-in bridge did not become ready")
48
+
49
+ server = GimpMcpServer(port=0)
50
+ try:
51
+ server.register_builtin_actions()
52
+ server.start(install_atexit_hook=False)
53
+ url = server.mcp_url
54
+ call(url, "load_skill", {"skill_name": "gimp-session"})
55
+ listed = post(url, "tools/list", {})["result"]["tools"]
56
+ names = {item["name"] for item in listed}
57
+ assert any(name.endswith("__get_status") for name in names)
58
+ assert call(url, next(name for name in names if name.endswith("__get_status")))["ready"]
59
+ images = call(url, next(name for name in names if name.endswith("__list_images")))
60
+ assert isinstance(images.get("images"), list)
61
+ print(json.dumps({"status": status, "images": images}))
62
+ finally:
63
+ server.stop()
64
+
65
+
66
+ if __name__ == "__main__":
67
+ main()
@@ -0,0 +1,24 @@
1
+ import json
2
+ import socket
3
+ import threading
4
+
5
+ from dcc_mcp_gimp.bridge import GimpBridge
6
+
7
+
8
+ def test_bridge_sends_json_lines_request():
9
+ received = {}
10
+
11
+ def serve(listener):
12
+ connection, _ = listener.accept()
13
+ with connection:
14
+ received["request"] = json.loads(connection.makefile("r", encoding="utf-8").readline())
15
+ connection.sendall(b'{"jsonrpc":"2.0","id":1,"result":{"ready":true}}\n')
16
+
17
+ listener = socket.socket()
18
+ listener.bind(("127.0.0.1", 0))
19
+ listener.listen(1)
20
+ threading.Thread(target=serve, args=(listener,), daemon=True).start()
21
+ bridge = GimpBridge(port=listener.getsockname()[1])
22
+ assert bridge.call("gimp.ping") == {"ready": True}
23
+ assert received["request"]["method"] == "gimp.ping"
24
+ listener.close()
@@ -0,0 +1,8 @@
1
+ from pathlib import Path
2
+
3
+ from dcc_mcp_core import validate_skill
4
+
5
+ skills_root = Path(__file__).parents[1] / "src" / "dcc_mcp_gimp" / "skills"
6
+ reports = [validate_skill(str(path)) for path in skills_root.iterdir() if path.is_dir()]
7
+ assert all(report.is_clean for report in reports), [report.issues for report in reports]
8
+ print(f"validated {len(reports)} bundled skills")