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.
- dcc_mcp_gimp-0.2.0/.gitignore +6 -0
- dcc_mcp_gimp-0.2.0/CHANGELOG.md +20 -0
- dcc_mcp_gimp-0.2.0/LICENSE +21 -0
- dcc_mcp_gimp-0.2.0/PKG-INFO +66 -0
- dcc_mcp_gimp-0.2.0/README.md +46 -0
- dcc_mcp_gimp-0.2.0/bridge/gimp-plugin/dcc_mcp_gimp.py +106 -0
- dcc_mcp_gimp-0.2.0/pyproject.toml +50 -0
- dcc_mcp_gimp-0.2.0/src/dcc_mcp_gimp/__init__.py +3 -0
- dcc_mcp_gimp-0.2.0/src/dcc_mcp_gimp/__version__.py +1 -0
- dcc_mcp_gimp-0.2.0/src/dcc_mcp_gimp/bridge.py +51 -0
- dcc_mcp_gimp-0.2.0/src/dcc_mcp_gimp/install.py +35 -0
- dcc_mcp_gimp-0.2.0/src/dcc_mcp_gimp/server.py +62 -0
- dcc_mcp_gimp-0.2.0/src/dcc_mcp_gimp/skills/gimp-session/SKILL.md +23 -0
- dcc_mcp_gimp-0.2.0/src/dcc_mcp_gimp/skills/gimp-session/metadata/depends.md +1 -0
- dcc_mcp_gimp-0.2.0/src/dcc_mcp_gimp/skills/gimp-session/scripts/get_active_image.py +15 -0
- dcc_mcp_gimp-0.2.0/src/dcc_mcp_gimp/skills/gimp-session/scripts/get_status.py +12 -0
- dcc_mcp_gimp-0.2.0/src/dcc_mcp_gimp/skills/gimp-session/scripts/list_images.py +13 -0
- dcc_mcp_gimp-0.2.0/src/dcc_mcp_gimp/skills/gimp-session/tools.yaml +34 -0
- dcc_mcp_gimp-0.2.0/tests/live_gimp_smoke.py +67 -0
- dcc_mcp_gimp-0.2.0/tests/test_bridge.py +24 -0
- dcc_mcp_gimp-0.2.0/tools/lint_skills.py +8 -0
|
@@ -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 @@
|
|
|
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 @@
|
|
|
1
|
+
- dcc-diagnostics
|
|
@@ -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")
|