gimp-mcp: clean-room BSD-3 GIMP PDB bridge

This commit is contained in:
Hanzo AI
2026-06-17 21:10:54 +00:00
commit 1a08d70cf9
7 changed files with 766 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
__pycache__/
*.py[cod]
*.egg-info/
.venv/
dist/
build/
*.xcf~
+28
View File
@@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (c) 2026 Hanzo AI
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+2
View File
@@ -0,0 +1,2 @@
hanzo-gimp-mcp -- clean-room BSD-3 implementation by Hanzo AI; no GPL lineage.
Drives GIMP via its documented PDB API.
+133
View File
@@ -0,0 +1,133 @@
# Hanzo GIMP Bridge Protocol
A line-oriented JSON protocol over a plain TCP socket. The bridge plug-in
(`hanzo_gimp_bridge.py`) runs inside GIMP 3.0 and listens, by default, on
`127.0.0.1:9876` (override with the `HANZO_GIMP_PORT` / `HANZO_GIMP_HOST`
environment variables before starting GIMP).
This is a **clean-room BSD-3** design. Every operation maps onto GIMP's own
documented Procedural Database (PDB) and the `Gimp.*` GObject-Introspection
API. There is no GPL lineage.
## Framing
* One JSON object per line, UTF-8, terminated by a single `\n`.
* The client sends one **request** object per line.
* The server replies with exactly one **response** object per request,
newline-terminated.
* Multiple requests may be pipelined on one connection.
### Request
```json
{"id": <any>, "method": "<string>", "params": { ... }}
```
* `id` — any JSON value; echoed back so clients can correlate replies.
* `method` — one of the methods below.
* `params` — method-specific object (may be omitted / empty).
### Response
Success:
```json
{"id": <any>, "result": <any>}
```
Error:
```json
{"id": <any>, "error": {"message": "<string>", "type": "<ExceptionClass>"}}
```
## Object ids
GIMP 3.0 PDB procedures operate on live objects (images, layers, drawables,
paths). Because JSON can only carry primitives, the bridge exchanges **integer
ids**. Where a PDB procedure expects an `Image`, `Item`, `Drawable`, `Layer`,
`Channel`, or `Path`, pass its integer id and the bridge rehydrates the live
object. Returned objects are likewise reduced to integer ids.
## Methods
| Method | Params | Result |
|-------------------|------------------------------------------|--------|
| `version` | — | `{bridge, gimp, protocol}` |
| `pdb_call` | `procedure: str`, `args: list` | the procedure's return value(s) (ids for objects), or `null` |
| `open_image` | `path: str` | `{image_id, width, height}` |
| `export` | `image_id: int`, `path: str` | `{image_id, path, saved}` |
| `new_image` | `width: int`, `height: int` | `{image_id, width, height}` |
| `image_info` | `image_id: int` | `{image_id, width, height, base_type, precision, layers, n_layers, file, dirty}` |
| `list_procedures` | `prefix?: str` | `{count, procedures: [str, ...]}` |
| `flatten` | `image_id: int` | `{image_id, layer_id}` |
### `pdb_call` — the core power
`pdb_call` is a generic invocation of any procedure in GIMP's PDB. Anything
GIMP can do is reachable through it. The bridge:
1. Looks up the procedure by name.
2. If the first PDB argument is a run-mode and you did not supply one,
prepends `NONINTERACTIVE`.
3. Coerces integer ids in `args` to the live `Image` / `Item` / `Gio.File`
objects the procedure expects.
4. Runs it and returns the values after the PDB status code (object results
reduced to ids). A non-`SUCCESS` status becomes an `error` response.
Use `list_procedures` to discover names (e.g. prefix `"gimp-image-"`).
## Examples
Assuming a raw TCP connection to `127.0.0.1:9876`:
**Version**
```
--> {"id": 1, "method": "version"}
<-- {"id": 1, "result": {"bridge": "1.0.0", "gimp": "3.0.0", "protocol": "hanzo-gimp/1"}}
```
**Open an image**
```
--> {"id": 2, "method": "open_image", "params": {"path": "/tmp/in.png"}}
<-- {"id": 2, "result": {"image_id": 1, "width": 1920, "height": 1080}}
```
**Generic PDB call — Gaussian blur on the active drawable**
```
--> {"id": 3, "method": "pdb_call",
"params": {"procedure": "plug-in-gauss", "args": [1, <drawable_id>, 15, 15, 0]}}
<-- {"id": 3, "result": null}
```
**Flatten and export**
```
--> {"id": 4, "method": "flatten", "params": {"image_id": 1}}
<-- {"id": 4, "result": {"image_id": 1, "layer_id": 7}}
--> {"id": 5, "method": "export", "params": {"image_id": 1, "path": "/tmp/out.png"}}
<-- {"id": 5, "result": {"image_id": 1, "path": "/tmp/out.png", "saved": true}}
```
**Discover procedures**
```
--> {"id": 6, "method": "list_procedures", "params": {"prefix": "gimp-image-"}}
<-- {"id": 6, "result": {"count": 42, "procedures": ["gimp-image-add-layer", ...]}}
```
**Error shape**
```
--> {"id": 7, "method": "pdb_call", "params": {"procedure": "no-such-proc", "args": []}}
<-- {"id": 7, "error": {"message": "unknown PDB procedure: no-such-proc", "type": "ValueError"}}
```
## Concurrency
GIMP/GEGL are single-threaded. The bridge accepts connections on worker
threads but marshals every request onto GIMP's GLib main loop, so operations
execute serially in the order GIMP receives them.
+101
View File
@@ -0,0 +1,101 @@
# hanzo-gimp-mcp
A **clean-room, BSD-3-licensed** bridge that lets external tools drive
[GIMP](https://www.gimp.org/) 3.0 programmatically.
It ships one GIMP 3.0 Python plug-in, `hanzo_gimp_bridge.py`, which registers
itself in GIMP and runs a small **JSON-over-TCP server** inside the running
GIMP process. Clients connect to that socket and invoke GIMP through its
documented **Procedural Database (PDB)** and `Gimp.*` GObject-Introspection
API.
* Protocol: see [`PROTOCOL.md`](./PROTOCOL.md).
* The Hanzo MCP `gimp` tool (TypeScript) and `hanzo-tools-gimp` (Python)
connect to this bridge and speak the same protocol.
## Clean-room / licensing
This implementation is an **original, clean-room rewrite**. It was designed
solely from GIMP's own public API documentation (the PDB and the GIMP 3.0
Python/GI bindings). It has **no GPL lineage** and reuses no code from any
prior GPL GIMP-MCP implementation.
* License: **BSD-3-Clause** (see [`LICENSE`](./LICENSE)).
* Copyright (c) 2026 Hanzo AI.
* See [`NOTICE`](./NOTICE).
GIMP itself remains GPL and is a separate program; this bridge merely talks to
it over a socket using its public, documented interfaces — the same way any
external automation client would.
## Install
GIMP 3.0 loads Python plug-ins from per-user plug-in directories. Each plug-in
must live in **its own folder whose name matches the script**, and the script
must be **executable**.
### Linux
```sh
mkdir -p ~/.config/GIMP/3.0/plug-ins/hanzo_gimp_bridge
cp hanzo_gimp_bridge.py ~/.config/GIMP/3.0/plug-ins/hanzo_gimp_bridge/
chmod +x ~/.config/GIMP/3.0/plug-ins/hanzo_gimp_bridge/hanzo_gimp_bridge.py
```
### macOS
```sh
mkdir -p "~/Library/Application Support/GIMP/3.0/plug-ins/hanzo_gimp_bridge"
cp hanzo_gimp_bridge.py "~/Library/Application Support/GIMP/3.0/plug-ins/hanzo_gimp_bridge/"
chmod +x "~/Library/Application Support/GIMP/3.0/plug-ins/hanzo_gimp_bridge/hanzo_gimp_bridge.py"
```
### Windows
Copy `hanzo_gimp_bridge.py` into:
```
%APPDATA%\GIMP\3.0\plug-ins\hanzo_gimp_bridge\hanzo_gimp_bridge.py
```
You can also confirm/extend the search paths in GIMP under
**Edit -> Preferences -> Folders -> Plug-ins**.
## Run
1. Start GIMP 3.0. (Optionally set the port first:
`HANZO_GIMP_PORT=9876 gimp`. `HANZO_GIMP_HOST` defaults to `127.0.0.1`.)
2. In GIMP, run **Filters -> Hanzo -> Start Hanzo Bridge**. GIMP shows a
message like `Hanzo GIMP bridge listening on 127.0.0.1:9876`.
3. The bridge now accepts JSON-line requests on that port.
A headless variant is possible with GIMP's batch mode; the simplest reliable
setup is an interactive GIMP session with the bridge started from the menu.
## Quick test
With the bridge running, from any shell:
```sh
printf '{"id":1,"method":"version"}\n' | nc 127.0.0.1 9876
```
You should get back a `version` result.
## Connecting from Hanzo
* **TypeScript MCP**: the `gimp` tool in `hanzo/mcp`
(`src/tools/gimp.ts`) exposes actions
(`version`, `open`, `export`, `pdb_call`, `new_image`, `image_info`,
`list_procedures`, `flatten`) and forwards them to this bridge.
* **Python**: `hanzo-tools-gimp` provides `GimpTool` with the same action
surface.
Both default to `127.0.0.1:9876` and accept `host` / `port` overrides.
## Methods at a glance
`version`, `pdb_call`, `open_image`, `export`, `new_image`, `image_info`,
`list_procedures`, `flatten`. `pdb_call` is the generic escape hatch: it can
invoke **any** PDB procedure, so anything GIMP can do is reachable. See
[`PROTOCOL.md`](./PROTOCOL.md) for full details and examples.
+467
View File
@@ -0,0 +1,467 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2026 Hanzo AI
#
# hanzo_gimp_bridge.py -- clean-room BSD-3 GIMP 3.0 bridge plug-in.
#
# This is a GIMP 3.0 Python plug-in (GObject-Introspection style) that runs a
# small line-oriented JSON TCP server inside GIMP. External clients (the Hanzo
# MCP "gimp" tool, hanzo-tools-gimp, or anything that can open a socket) drive
# GIMP entirely through its documented Procedural Database (PDB) and the
# Gimp.* introspected API. No GPL lineage: every call below is derived from
# GIMP's own public API documentation.
#
# Protocol (see PROTOCOL.md):
# request : {"id": <any>, "method": <str>, "params": {<...>}}\n
# response : {"id": <any>, "result": <any>}\n
# or {"id": <any>, "error": {"message": <str>, "type": <str>}}\n
# One JSON object per line, UTF-8, newline-terminated.
from __future__ import annotations
import json
import os
import socket
import sys
import threading
import traceback
from typing import Any
import gi
gi.require_version("Gimp", "3.0")
from gi.repository import Gimp, Gio, GLib, GObject # noqa: E402
# ---------------------------------------------------------------------------
# Defaults / configuration
# ---------------------------------------------------------------------------
DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 9876
BRIDGE_VERSION = "1.0.0"
# Procedure registered inside GIMP to start the bridge from the Filters menu.
PROC_START = "hanzo-gimp-bridge-start"
# ---------------------------------------------------------------------------
# GIMP value <-> JSON marshalling
# ---------------------------------------------------------------------------
def _gimp_object_to_id(obj: Any) -> Any:
"""Reduce a GIMP object (image/drawable/layer/...) to its integer id.
GIMP 3.0 PDB procedures accept/return live objects, but a JSON wire
protocol can only carry primitives, so we exchange integer ids and
rehydrate them on demand via ``Gimp.Image.get_by_id`` and friends.
"""
if obj is None:
return None
for getter in ("get_id",):
fn = getattr(obj, getter, None)
if callable(fn):
try:
return int(fn())
except Exception:
pass
return obj
def _json_default(obj: Any) -> Any:
"""Best-effort JSON fallback for non-primitive GIMP/GLib values."""
reduced = _gimp_object_to_id(obj)
if reduced is not obj:
return reduced
if isinstance(obj, (bytes, bytearray)):
return list(obj)
return str(obj)
def _image_by_id(image_id: int) -> Gimp.Image:
img = Gimp.Image.get_by_id(int(image_id))
if img is None:
raise ValueError(f"no image with id {image_id}")
return img
# ---------------------------------------------------------------------------
# Generic PDB invocation
# ---------------------------------------------------------------------------
def _coerce_arg(spec: GObject.ParamSpec, value: Any) -> Any:
"""Coerce a JSON value to the type a PDB argument expects.
Integer ids are rehydrated into the GIMP object the PDB wants
(Image / Drawable / Layer / Item), so callers can pass plain ints.
"""
if value is None:
return None
value_type = spec.value_type
# Object arguments: accept an int id, fetch the live object.
if GObject.type_is_a(value_type, Gimp.Image.__gtype__):
return Gimp.Image.get_by_id(int(value))
if GObject.type_is_a(value_type, Gimp.Item.__gtype__):
# Covers Drawable, Layer, Channel, Vectors/Path -- all Items.
return Gimp.Item.get_by_id(int(value))
if GObject.type_is_a(value_type, Gio.File.__gtype__):
return Gio.File.new_for_path(str(value))
return value
def _pdb_call(procedure: str, args: list[Any]) -> Any:
"""Invoke an arbitrary PDB procedure -- the core power of the bridge.
Args:
procedure: PDB procedure name, e.g. "gimp-image-flatten".
args: Positional arguments in PDB order. Integer ids are accepted
where the procedure expects a GIMP object.
Returns:
``None`` if the procedure returns only a status,
a single value if it returns exactly one result,
a list of values if it returns several.
GIMP objects are reduced to their integer ids.
"""
pdb = Gimp.get_pdb()
proc = pdb.lookup_procedure(procedure)
if proc is None:
raise ValueError(f"unknown PDB procedure: {procedure}")
arg_specs = proc.get_arguments()
config = proc.create_config()
# The first PDB arg of most procedures is a run-mode; default it to
# NONINTERACTIVE when the caller did not supply it.
supplied = list(args)
if arg_specs and arg_specs[0].value_type == Gimp.RunMode.__gtype__:
if len(supplied) < len(arg_specs):
supplied = [int(Gimp.RunMode.NONINTERACTIVE)] + supplied
for spec, value in zip(arg_specs, supplied):
config.set_property(spec.get_name(), _coerce_arg(spec, value))
result = proc.run(config)
status = result.index(0)
if status != Gimp.PDBStatusType.SUCCESS:
msg = ""
try:
msg = result.index(1)
except Exception:
pass
raise RuntimeError(f"PDB {procedure} failed: {status.value_nick}: {msg}")
# Collect return values after the status code.
out: list[Any] = []
for i in range(1, result.length()):
out.append(_gimp_object_to_id(result.index(i)))
if not out:
return None
if len(out) == 1:
return out[0]
return out
# ---------------------------------------------------------------------------
# High-level convenience methods
# ---------------------------------------------------------------------------
def _open_image(path: str) -> dict[str, Any]:
gfile = Gio.File.new_for_path(path)
image = Gimp.file_load(Gimp.RunMode.NONINTERACTIVE, gfile)
return {
"image_id": _gimp_object_to_id(image),
"width": image.get_width(),
"height": image.get_height(),
}
def _export(image_id: int, path: str) -> dict[str, Any]:
image = _image_by_id(image_id)
gfile = Gio.File.new_for_path(path)
# Gimp.file_save dispatches to the right exporter by file extension and
# works for the common formats (png/jpg/tiff/webp/...). It exports a
# flattened copy for non-native formats; the .xcf path is GIMP-native.
Gimp.file_save(Gimp.RunMode.NONINTERACTIVE, image, gfile, None)
return {"image_id": int(image_id), "path": path, "saved": True}
def _new_image(width: int, height: int) -> dict[str, Any]:
image = Gimp.Image.new(int(width), int(height), Gimp.ImageBaseType.RGB)
return {
"image_id": _gimp_object_to_id(image),
"width": image.get_width(),
"height": image.get_height(),
}
def _image_info(image_id: int) -> dict[str, Any]:
image = _image_by_id(image_id)
layers = [_gimp_object_to_id(l) for l in image.get_layers()]
gfile = image.get_file()
return {
"image_id": int(image_id),
"width": image.get_width(),
"height": image.get_height(),
"base_type": image.get_base_type().value_nick,
"precision": image.get_precision().value_nick,
"layers": layers,
"n_layers": len(layers),
"file": gfile.get_path() if gfile is not None else None,
"dirty": bool(image.is_dirty()),
}
def _list_procedures(prefix: str | None = None) -> dict[str, Any]:
pdb = Gimp.get_pdb()
names = list(pdb.query_procedures("", "", "", "", "", "", ""))
if prefix:
names = [n for n in names if n.startswith(prefix)]
names.sort()
return {"count": len(names), "procedures": names}
def _flatten(image_id: int) -> dict[str, Any]:
image = _image_by_id(image_id)
layer = image.flatten()
return {"image_id": int(image_id), "layer_id": _gimp_object_to_id(layer)}
def _version() -> dict[str, Any]:
return {
"bridge": BRIDGE_VERSION,
"gimp": Gimp.version(),
"protocol": "hanzo-gimp/1",
}
# ---------------------------------------------------------------------------
# Method dispatch
# ---------------------------------------------------------------------------
def dispatch(method: str, params: dict[str, Any]) -> Any:
"""Route a single JSON-protocol request to its handler."""
params = params or {}
if method == "version":
return _version()
if method == "pdb_call":
return _pdb_call(params["procedure"], params.get("args", []))
if method == "open_image":
return _open_image(params["path"])
if method == "export":
return _export(params["image_id"], params["path"])
if method == "new_image":
return _new_image(params["width"], params["height"])
if method == "image_info":
return _image_info(params["image_id"])
if method == "list_procedures":
return _list_procedures(params.get("prefix"))
if method == "flatten":
return _flatten(params["image_id"])
raise ValueError(f"unknown method: {method}")
# ---------------------------------------------------------------------------
# TCP JSON line server
# ---------------------------------------------------------------------------
class BridgeServer:
"""Line-oriented JSON TCP server that runs GIMP requests on the main loop.
GIMP/GEGL are not thread-safe, so every request is marshalled onto GIMP's
GLib main loop via ``GLib.idle_add`` and the socket thread blocks on the
result. This keeps all PDB work on the thread GIMP expects.
"""
def __init__(self, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT):
self.host = host
self.port = port
self._sock: socket.socket | None = None
self._thread: threading.Thread | None = None
self._running = False
def start(self) -> None:
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._sock.bind((self.host, self.port))
self._sock.listen(8)
self._running = True
self._thread = threading.Thread(target=self._accept_loop, daemon=True)
self._thread.start()
sys.stderr.write(
f"[hanzo-gimp-bridge] listening on {self.host}:{self.port}\n"
)
sys.stderr.flush()
def _accept_loop(self) -> None:
assert self._sock is not None
while self._running:
try:
conn, _addr = self._sock.accept()
except OSError:
break
threading.Thread(
target=self._handle_conn, args=(conn,), daemon=True
).start()
def _handle_conn(self, conn: socket.socket) -> None:
with conn:
buf = b""
while self._running:
try:
chunk = conn.recv(65536)
except OSError:
break
if not chunk:
break
buf += chunk
while b"\n" in buf:
line, buf = buf.split(b"\n", 1)
if not line.strip():
continue
reply = self._process_line(line)
try:
conn.sendall(reply)
except OSError:
return
def _process_line(self, line: bytes) -> bytes:
req_id: Any = None
try:
req = json.loads(line.decode("utf-8"))
req_id = req.get("id")
method = req.get("method")
params = req.get("params") or {}
if not method:
raise ValueError("missing method")
result = self._run_on_main(method, params)
payload = {"id": req_id, "result": result}
except Exception as exc: # noqa: BLE001 -- report any failure to client
payload = {
"id": req_id,
"error": {
"message": str(exc),
"type": type(exc).__name__,
},
}
return (json.dumps(payload, default=_json_default) + "\n").encode("utf-8")
def _run_on_main(self, method: str, params: dict[str, Any]) -> Any:
"""Run ``dispatch`` on GIMP's main loop and block for the result."""
done = threading.Event()
box: dict[str, Any] = {}
def _invoke() -> bool:
try:
box["result"] = dispatch(method, params)
except Exception as exc: # noqa: BLE001
box["error"] = exc
box["traceback"] = traceback.format_exc()
finally:
done.set()
return GLib.SOURCE_REMOVE
GLib.idle_add(_invoke)
done.wait()
if "error" in box:
sys.stderr.write(box.get("traceback", ""))
sys.stderr.flush()
raise box["error"]
return box.get("result")
def stop(self) -> None:
self._running = False
if self._sock is not None:
try:
self._sock.close()
except OSError:
pass
# Keep a module-level reference so the server outlives do_run.
_SERVER: BridgeServer | None = None
def _resolve_port() -> int:
env = os.environ.get("HANZO_GIMP_PORT")
if env:
try:
return int(env)
except ValueError:
pass
return DEFAULT_PORT
def _resolve_host() -> str:
return os.environ.get("HANZO_GIMP_HOST", DEFAULT_HOST)
def start_bridge() -> BridgeServer:
"""Start (once) and return the module-level bridge server."""
global _SERVER
if _SERVER is None:
_SERVER = BridgeServer(host=_resolve_host(), port=_resolve_port())
_SERVER.start()
return _SERVER
# ---------------------------------------------------------------------------
# GIMP 3.0 plug-in registration
# ---------------------------------------------------------------------------
class HanzoGimpBridge(Gimp.PlugIn):
"""GIMP 3.0 plug-in that exposes GIMP over a JSON TCP socket."""
# -- Gimp.PlugIn vfuncs --------------------------------------------------
def do_query_procedures(self) -> list[str]:
return [PROC_START]
def do_create_procedure(self, name: str) -> Gimp.Procedure:
if name != PROC_START:
return None
procedure = Gimp.Procedure.new(
self,
name,
Gimp.PDBProcType.PLUGIN,
self.run_start,
None,
)
procedure.set_menu_label("Start Hanzo Bridge")
procedure.add_menu_path("<Image>/Filters/Hanzo")
procedure.set_documentation(
"Start the Hanzo GIMP JSON socket bridge",
"Runs a local JSON-over-TCP server that drives GIMP through its "
"PDB API. Clean-room BSD-3, no GPL lineage.",
name,
)
procedure.set_attribution("Hanzo AI", "Hanzo AI", "2026")
return procedure
# -- Procedure run callback ---------------------------------------------
def run_start(self, procedure, run_mode, image, drawables, config, run_data):
try:
server = start_bridge()
Gimp.message(
f"Hanzo GIMP bridge listening on "
f"{server.host}:{server.port}"
)
return procedure.new_return_values(
Gimp.PDBStatusType.SUCCESS, GLib.Error()
)
except Exception as exc: # noqa: BLE001
return procedure.new_return_values(
Gimp.PDBStatusType.EXECUTION_ERROR,
GLib.Error(str(exc)),
)
Gimp.main(HanzoGimpBridge.__gtype__, sys.argv)
+28
View File
@@ -0,0 +1,28 @@
[build-system]
requires = ["setuptools>=61.0.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "hanzo-gimp-mcp"
version = "1.0.0"
description = "Clean-room BSD-3 GIMP 3.0 bridge: a GIMP plug-in exposing a JSON-over-TCP protocol that drives GIMP through its documented PDB API."
readme = "README.md"
requires-python = ">=3.10"
license = { text = "BSD-3-Clause" }
authors = [{ name = "Hanzo AI", email = "dev@hanzo.ai" }]
keywords = ["hanzo", "gimp", "mcp", "pdb", "image", "bridge", "bsd-3-clause"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Programming Language :: Python :: 3",
"Topic :: Multimedia :: Graphics",
]
[project.urls]
Homepage = "https://github.com/hanzoai/gimp-mcp"
Repository = "https://github.com/hanzoai/gimp-mcp"
# This package ships a single GIMP 3.0 plug-in module that you copy into the
# GIMP plug-ins directory (see README). It is not a conventional importable
# library, so no package discovery is configured.