hanzo-tools-gimp 1.0.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.
- hanzo_tools_gimp-1.0.0/.gitignore +76 -0
- hanzo_tools_gimp-1.0.0/PKG-INFO +90 -0
- hanzo_tools_gimp-1.0.0/README.md +64 -0
- hanzo_tools_gimp-1.0.0/hanzo_tools/__init__.py +4 -0
- hanzo_tools_gimp-1.0.0/hanzo_tools/gimp/__init__.py +64 -0
- hanzo_tools_gimp-1.0.0/hanzo_tools/gimp/gimp_tool.py +299 -0
- hanzo_tools_gimp-1.0.0/pyproject.toml +66 -0
- hanzo_tools_gimp-1.0.0/tests/test_gimp_tool.py +123 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# IDE and editor
|
|
2
|
+
.vscode
|
|
3
|
+
.idea
|
|
4
|
+
|
|
5
|
+
# Python
|
|
6
|
+
*.egg-info
|
|
7
|
+
__pycache__
|
|
8
|
+
.mypy_cache
|
|
9
|
+
.pytest_cache
|
|
10
|
+
*.pyc
|
|
11
|
+
.venv
|
|
12
|
+
|
|
13
|
+
# Build
|
|
14
|
+
build/
|
|
15
|
+
dist
|
|
16
|
+
_dev
|
|
17
|
+
|
|
18
|
+
# Environment
|
|
19
|
+
.env
|
|
20
|
+
.envrc
|
|
21
|
+
|
|
22
|
+
# Logs
|
|
23
|
+
.prism.log
|
|
24
|
+
codegen.log
|
|
25
|
+
*.log
|
|
26
|
+
|
|
27
|
+
# Package manager
|
|
28
|
+
Brewfile.lock.json
|
|
29
|
+
|
|
30
|
+
# Agent config files (symlinked from user home)
|
|
31
|
+
LLM.md
|
|
32
|
+
AGENTS.md
|
|
33
|
+
CLAUDE.md
|
|
34
|
+
GEMINI.md
|
|
35
|
+
GROK.md
|
|
36
|
+
QWEN.md
|
|
37
|
+
|
|
38
|
+
# Local databases and state
|
|
39
|
+
.hanzo/
|
|
40
|
+
.grok/
|
|
41
|
+
|
|
42
|
+
# Documentation build
|
|
43
|
+
docs/.next/
|
|
44
|
+
docs/out/
|
|
45
|
+
docs/node_modules/
|
|
46
|
+
|
|
47
|
+
# Training data and scripts (DO NOT COMMIT)
|
|
48
|
+
training_dataset.jsonl
|
|
49
|
+
scripts/full_extractor.py
|
|
50
|
+
scripts/mega_extractor.py
|
|
51
|
+
scripts/mega_full_extractor.py
|
|
52
|
+
scripts/streaming_extractor.py
|
|
53
|
+
scripts/supplement_extractor.py
|
|
54
|
+
|
|
55
|
+
# Test files at root (experimental)
|
|
56
|
+
test_post_quantum_*.py
|
|
57
|
+
|
|
58
|
+
# Analysis documents (internal)
|
|
59
|
+
HANZO_INNOVATION_OPPORTUNITIES.md
|
|
60
|
+
POST_QUANTUM_CRYPTOGRAPHY_IMPLEMENTATION.md
|
|
61
|
+
|
|
62
|
+
# Experimental cryptography (WIP)
|
|
63
|
+
pkg/hanzo/src/hanzo/cryptography/
|
|
64
|
+
site/
|
|
65
|
+
|
|
66
|
+
# hygiene (untrack node_modules, block common build output)
|
|
67
|
+
node_modules/
|
|
68
|
+
**/node_modules/
|
|
69
|
+
.pnpm-store/
|
|
70
|
+
dist/
|
|
71
|
+
.next/
|
|
72
|
+
coverage/
|
|
73
|
+
playwright-report/
|
|
74
|
+
test-results/
|
|
75
|
+
tmp/
|
|
76
|
+
.DS_Store
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hanzo-tools-gimp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Unified GIMP automation tool for Hanzo AI (HIP-0300) — clean-room BSD-3 client for the GIMP PDB bridge
|
|
5
|
+
Project-URL: Homepage, https://github.com/hanzoai/python-sdk
|
|
6
|
+
Project-URL: Documentation, https://docs.hanzo.ai
|
|
7
|
+
Project-URL: Repository, https://github.com/hanzoai/python-sdk
|
|
8
|
+
Author-email: Hanzo AI <dev@hanzo.ai>
|
|
9
|
+
License: BSD-3-Clause
|
|
10
|
+
Keywords: automation,gimp,hanzo,image,mcp,pdb,tools
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Requires-Dist: hanzo-tools>=0.3.0
|
|
19
|
+
Requires-Dist: mcp>=1.0.0
|
|
20
|
+
Requires-Dist: pydantic>=2.0.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# hanzo-tools-gimp
|
|
28
|
+
|
|
29
|
+
Unified **GIMP automation** tool for Hanzo AI (HIP-0300).
|
|
30
|
+
|
|
31
|
+
Drives [GIMP](https://www.gimp.org/) 3.0 through the **Hanzo GIMP bridge** — a
|
|
32
|
+
clean-room, **BSD-3** GIMP plug-in (see `hanzo/gimp-mcp`) that exposes GIMP's
|
|
33
|
+
Procedural Database (PDB) over a line-oriented JSON TCP socket. This package is
|
|
34
|
+
the Python client for that bridge.
|
|
35
|
+
|
|
36
|
+
This is a clean-room implementation with **no GPL lineage**: both the bridge
|
|
37
|
+
and this client use GIMP's documented, public PDB / GObject-Introspection API.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
pip install hanzo-tools-gimp
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Prerequisites
|
|
46
|
+
|
|
47
|
+
The Hanzo GIMP bridge plug-in must be running inside GIMP. Briefly:
|
|
48
|
+
|
|
49
|
+
1. Copy `hanzo_gimp_bridge.py` (from `hanzo/gimp-mcp`) into your GIMP 3.0
|
|
50
|
+
plug-ins directory (one folder per plug-in, script made executable).
|
|
51
|
+
2. Start GIMP and run **Filters → Hanzo → Start Hanzo Bridge**.
|
|
52
|
+
3. The bridge listens on `127.0.0.1:9876` by default (override with
|
|
53
|
+
`HANZO_GIMP_PORT` / `HANZO_GIMP_HOST`).
|
|
54
|
+
|
|
55
|
+
See the `gimp-mcp` repo's `README.md` and `PROTOCOL.md` for full details.
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from hanzo_tools.gimp import GimpTool, register_tools, TOOLS
|
|
61
|
+
|
|
62
|
+
# Register with a FastMCP server
|
|
63
|
+
register_tools(mcp_server) # optional host=..., port=...
|
|
64
|
+
|
|
65
|
+
# Or use the tool directly
|
|
66
|
+
tool = GimpTool(host="127.0.0.1", port=9876)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The tool exposes the same action surface as the Hanzo MCP TypeScript `gimp`
|
|
70
|
+
tool:
|
|
71
|
+
|
|
72
|
+
| Action | Params | Result |
|
|
73
|
+
|-------------------|---------------------------------|--------|
|
|
74
|
+
| `version` | — | `{bridge, gimp, protocol}` |
|
|
75
|
+
| `open` | `path` | `{image_id, width, height}` |
|
|
76
|
+
| `export` | `image_id`, `path` | `{image_id, path, saved}` |
|
|
77
|
+
| `pdb_call` | `procedure`, `args` | the procedure's return value(s) |
|
|
78
|
+
| `new_image` | `width`, `height` | `{image_id, width, height}` |
|
|
79
|
+
| `image_info` | `image_id` | `{image_id, width, height, ...}` |
|
|
80
|
+
| `list_procedures` | `prefix?` | `{count, procedures}` |
|
|
81
|
+
| `flatten` | `image_id` | `{image_id, layer_id}` |
|
|
82
|
+
|
|
83
|
+
Every action also accepts `host` / `port` overrides.
|
|
84
|
+
|
|
85
|
+
`pdb_call` is the generic escape hatch — it can invoke **any** PDB procedure,
|
|
86
|
+
so anything GIMP can do is reachable.
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
**BSD-3-Clause**. Copyright (c) 2026 Hanzo AI.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# hanzo-tools-gimp
|
|
2
|
+
|
|
3
|
+
Unified **GIMP automation** tool for Hanzo AI (HIP-0300).
|
|
4
|
+
|
|
5
|
+
Drives [GIMP](https://www.gimp.org/) 3.0 through the **Hanzo GIMP bridge** — a
|
|
6
|
+
clean-room, **BSD-3** GIMP plug-in (see `hanzo/gimp-mcp`) that exposes GIMP's
|
|
7
|
+
Procedural Database (PDB) over a line-oriented JSON TCP socket. This package is
|
|
8
|
+
the Python client for that bridge.
|
|
9
|
+
|
|
10
|
+
This is a clean-room implementation with **no GPL lineage**: both the bridge
|
|
11
|
+
and this client use GIMP's documented, public PDB / GObject-Introspection API.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pip install hanzo-tools-gimp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Prerequisites
|
|
20
|
+
|
|
21
|
+
The Hanzo GIMP bridge plug-in must be running inside GIMP. Briefly:
|
|
22
|
+
|
|
23
|
+
1. Copy `hanzo_gimp_bridge.py` (from `hanzo/gimp-mcp`) into your GIMP 3.0
|
|
24
|
+
plug-ins directory (one folder per plug-in, script made executable).
|
|
25
|
+
2. Start GIMP and run **Filters → Hanzo → Start Hanzo Bridge**.
|
|
26
|
+
3. The bridge listens on `127.0.0.1:9876` by default (override with
|
|
27
|
+
`HANZO_GIMP_PORT` / `HANZO_GIMP_HOST`).
|
|
28
|
+
|
|
29
|
+
See the `gimp-mcp` repo's `README.md` and `PROTOCOL.md` for full details.
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from hanzo_tools.gimp import GimpTool, register_tools, TOOLS
|
|
35
|
+
|
|
36
|
+
# Register with a FastMCP server
|
|
37
|
+
register_tools(mcp_server) # optional host=..., port=...
|
|
38
|
+
|
|
39
|
+
# Or use the tool directly
|
|
40
|
+
tool = GimpTool(host="127.0.0.1", port=9876)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The tool exposes the same action surface as the Hanzo MCP TypeScript `gimp`
|
|
44
|
+
tool:
|
|
45
|
+
|
|
46
|
+
| Action | Params | Result |
|
|
47
|
+
|-------------------|---------------------------------|--------|
|
|
48
|
+
| `version` | — | `{bridge, gimp, protocol}` |
|
|
49
|
+
| `open` | `path` | `{image_id, width, height}` |
|
|
50
|
+
| `export` | `image_id`, `path` | `{image_id, path, saved}` |
|
|
51
|
+
| `pdb_call` | `procedure`, `args` | the procedure's return value(s) |
|
|
52
|
+
| `new_image` | `width`, `height` | `{image_id, width, height}` |
|
|
53
|
+
| `image_info` | `image_id` | `{image_id, width, height, ...}` |
|
|
54
|
+
| `list_procedures` | `prefix?` | `{count, procedures}` |
|
|
55
|
+
| `flatten` | `image_id` | `{image_id, layer_id}` |
|
|
56
|
+
|
|
57
|
+
Every action also accepts `host` / `port` overrides.
|
|
58
|
+
|
|
59
|
+
`pdb_call` is the generic escape hatch — it can invoke **any** PDB procedure,
|
|
60
|
+
so anything GIMP can do is reachable.
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
**BSD-3-Clause**. Copyright (c) 2026 Hanzo AI.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""GIMP automation tools for Hanzo AI (HIP-0300).
|
|
2
|
+
|
|
3
|
+
Drives GIMP 3.0 through the Hanzo GIMP bridge -- a clean-room, BSD-3 GIMP
|
|
4
|
+
plug-in that exposes GIMP's Procedural Database (PDB) over a JSON TCP socket.
|
|
5
|
+
No GPL lineage.
|
|
6
|
+
|
|
7
|
+
Tools:
|
|
8
|
+
- gimp: Unified GIMP automation tool (HIP-0300)
|
|
9
|
+
- version: Bridge + GIMP version
|
|
10
|
+
- open: Load an image file
|
|
11
|
+
- export: Export/save an image
|
|
12
|
+
- pdb_call: Invoke any PDB procedure (the core power)
|
|
13
|
+
- new_image: Create a blank RGB image
|
|
14
|
+
- image_info: Inspect an image
|
|
15
|
+
- list_procedures: Enumerate PDB procedures
|
|
16
|
+
- flatten: Flatten an image
|
|
17
|
+
|
|
18
|
+
Install:
|
|
19
|
+
pip install hanzo-tools-gimp
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
from hanzo_tools.gimp import register_tools, TOOLS, GimpTool
|
|
23
|
+
|
|
24
|
+
# Register with an MCP server
|
|
25
|
+
register_tools(mcp_server)
|
|
26
|
+
|
|
27
|
+
# Or use the unified tool directly
|
|
28
|
+
tool = GimpTool(host="127.0.0.1", port=9876)
|
|
29
|
+
|
|
30
|
+
Requires the Hanzo GIMP bridge plug-in (hanzo/gimp-mcp) running inside GIMP.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from hanzo_tools.core import BaseTool, ToolRegistry
|
|
34
|
+
|
|
35
|
+
from .gimp_tool import DEFAULT_HOST, DEFAULT_PORT, GimpTool, gimp_tool
|
|
36
|
+
|
|
37
|
+
# Export list for tool discovery - HIP-0300 unified tool
|
|
38
|
+
TOOLS = [GimpTool]
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"GimpTool",
|
|
42
|
+
"gimp_tool",
|
|
43
|
+
"DEFAULT_HOST",
|
|
44
|
+
"DEFAULT_PORT",
|
|
45
|
+
"register_tools",
|
|
46
|
+
"TOOLS",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def register_tools(mcp_server, **kwargs) -> list[BaseTool]:
|
|
51
|
+
"""Register gimp tools with the MCP server.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
mcp_server: The FastMCP server instance.
|
|
55
|
+
**kwargs: Optional ``host`` / ``port`` for the GIMP bridge.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
List of registered tool instances.
|
|
59
|
+
"""
|
|
60
|
+
host = kwargs.get("host")
|
|
61
|
+
port = kwargs.get("port")
|
|
62
|
+
tool = GimpTool(host=host, port=port)
|
|
63
|
+
ToolRegistry.register_tool(mcp_server, tool)
|
|
64
|
+
return [tool]
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""Unified GIMP automation tool for the HIP-0300 architecture.
|
|
2
|
+
|
|
3
|
+
This module provides a single unified ``gimp`` tool that drives GIMP 3.0
|
|
4
|
+
through the Hanzo GIMP bridge -- a clean-room, BSD-3 GIMP plug-in that exposes
|
|
5
|
+
GIMP's Procedural Database (PDB) over a line-oriented JSON TCP socket.
|
|
6
|
+
|
|
7
|
+
Actions (matching the TypeScript ``gimp`` tool):
|
|
8
|
+
- version: bridge + GIMP version
|
|
9
|
+
- open: load an image file
|
|
10
|
+
- export: export/save an image to a path
|
|
11
|
+
- pdb_call: invoke any PDB procedure (the core power)
|
|
12
|
+
- new_image: create a blank RGB image
|
|
13
|
+
- image_info: inspect an image
|
|
14
|
+
- list_procedures: enumerate PDB procedures (optional prefix filter)
|
|
15
|
+
- flatten: flatten an image
|
|
16
|
+
|
|
17
|
+
Following Unix philosophy: one tool for the GIMP-automation axis. No GPL
|
|
18
|
+
lineage -- the bridge and this client speak GIMP's documented public API.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
from typing import Any, ClassVar
|
|
25
|
+
|
|
26
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
27
|
+
|
|
28
|
+
from hanzo_tools.core import (
|
|
29
|
+
BaseTool,
|
|
30
|
+
InvalidParamsError,
|
|
31
|
+
ToolError,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
35
|
+
DEFAULT_PORT = 9876
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class GimpTool(BaseTool):
|
|
39
|
+
"""Unified GIMP automation tool (HIP-0300).
|
|
40
|
+
|
|
41
|
+
Connects to the Hanzo GIMP bridge over TCP and drives GIMP through its
|
|
42
|
+
PDB. The bridge default endpoint is ``127.0.0.1:9876``; every action
|
|
43
|
+
accepts ``host`` / ``port`` overrides.
|
|
44
|
+
|
|
45
|
+
Actions: version, open, export, pdb_call, new_image, image_info,
|
|
46
|
+
list_procedures, flatten.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
name: ClassVar[str] = "gimp"
|
|
50
|
+
VERSION: ClassVar[str] = "1.0.0"
|
|
51
|
+
|
|
52
|
+
def __init__(self, host: str | None = None, port: int | None = None):
|
|
53
|
+
super().__init__()
|
|
54
|
+
self.host = host or os.environ.get("HANZO_GIMP_HOST", DEFAULT_HOST)
|
|
55
|
+
env_port = os.environ.get("HANZO_GIMP_PORT")
|
|
56
|
+
self.port = port or (int(env_port) if env_port else DEFAULT_PORT)
|
|
57
|
+
self._register_gimp_actions()
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def description(self) -> str:
|
|
61
|
+
return """Unified GIMP automation tool (HIP-0300).
|
|
62
|
+
|
|
63
|
+
Drives GIMP 3.0 via the clean-room BSD-3 bridge (JSON-over-TCP to the GIMP PDB).
|
|
64
|
+
|
|
65
|
+
Actions:
|
|
66
|
+
- version: Bridge and GIMP version
|
|
67
|
+
- open: Load an image file (returns image_id, width, height)
|
|
68
|
+
- export: Export/save an image to a path
|
|
69
|
+
- pdb_call: Invoke any PDB procedure -- the generic core power
|
|
70
|
+
- new_image: Create a blank RGB image
|
|
71
|
+
- image_info: Inspect an image (size, layers, precision, file, dirty)
|
|
72
|
+
- list_procedures: Enumerate PDB procedures (optional prefix filter)
|
|
73
|
+
- flatten: Flatten an image
|
|
74
|
+
|
|
75
|
+
Requires the Hanzo GIMP bridge plug-in running inside GIMP
|
|
76
|
+
(default 127.0.0.1:9876).
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
async def _bridge_request(
|
|
80
|
+
self,
|
|
81
|
+
method: str,
|
|
82
|
+
params: dict[str, Any],
|
|
83
|
+
host: str | None = None,
|
|
84
|
+
port: int | None = None,
|
|
85
|
+
timeout: float = 60.0,
|
|
86
|
+
) -> Any:
|
|
87
|
+
"""Send one JSON request to the GIMP bridge and return its result.
|
|
88
|
+
|
|
89
|
+
Opens a short-lived connection, writes one newline-terminated JSON
|
|
90
|
+
request, reads one newline-terminated JSON reply.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
ToolError: on connection failure, timeout, malformed reply, or an
|
|
94
|
+
error response from the bridge.
|
|
95
|
+
"""
|
|
96
|
+
host = host or self.host
|
|
97
|
+
port = port or self.port
|
|
98
|
+
request = json.dumps({"id": 1, "method": method, "params": params}) + "\n"
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
reader, writer = await asyncio.wait_for(
|
|
102
|
+
asyncio.open_connection(host, port), timeout=timeout
|
|
103
|
+
)
|
|
104
|
+
except (OSError, asyncio.TimeoutError) as exc:
|
|
105
|
+
raise ToolError(
|
|
106
|
+
code="NOT_FOUND",
|
|
107
|
+
message=f"GIMP bridge connection failed ({host}:{port}): {exc}",
|
|
108
|
+
details={"host": host, "port": port},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
writer.write(request.encode("utf-8"))
|
|
113
|
+
await writer.drain()
|
|
114
|
+
line = await asyncio.wait_for(reader.readline(), timeout=timeout)
|
|
115
|
+
except asyncio.TimeoutError:
|
|
116
|
+
raise ToolError(code="TIMEOUT", message="GIMP bridge timed out")
|
|
117
|
+
finally:
|
|
118
|
+
writer.close()
|
|
119
|
+
try:
|
|
120
|
+
await writer.wait_closed()
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
if not line:
|
|
125
|
+
raise ToolError(
|
|
126
|
+
code="INTERNAL_ERROR", message="GIMP bridge closed without replying"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
msg = json.loads(line.decode("utf-8"))
|
|
131
|
+
except json.JSONDecodeError as exc:
|
|
132
|
+
raise ToolError(
|
|
133
|
+
code="INTERNAL_ERROR",
|
|
134
|
+
message=f"Invalid response from GIMP bridge: {exc}",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if isinstance(msg, dict) and msg.get("error"):
|
|
138
|
+
err = msg["error"]
|
|
139
|
+
raise ToolError(
|
|
140
|
+
code="INTERNAL_ERROR",
|
|
141
|
+
message=err.get("message", "GIMP bridge error"),
|
|
142
|
+
details={"type": err.get("type")},
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return msg.get("result") if isinstance(msg, dict) else msg
|
|
146
|
+
|
|
147
|
+
def _register_gimp_actions(self):
|
|
148
|
+
"""Register all GIMP actions."""
|
|
149
|
+
|
|
150
|
+
@self.action("version", "Get bridge and GIMP version")
|
|
151
|
+
async def version(
|
|
152
|
+
ctx: MCPContext,
|
|
153
|
+
host: str | None = None,
|
|
154
|
+
port: int | None = None,
|
|
155
|
+
) -> dict:
|
|
156
|
+
"""Return the bridge version, GIMP version, and protocol id."""
|
|
157
|
+
return await self._bridge_request("version", {}, host, port)
|
|
158
|
+
|
|
159
|
+
@self.action("open", "Open (load) an image file")
|
|
160
|
+
async def open_image(
|
|
161
|
+
ctx: MCPContext,
|
|
162
|
+
path: str,
|
|
163
|
+
host: str | None = None,
|
|
164
|
+
port: int | None = None,
|
|
165
|
+
) -> dict:
|
|
166
|
+
"""Load an image from disk.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
path: Filesystem path to the image.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
``{image_id, width, height}``.
|
|
173
|
+
"""
|
|
174
|
+
if not path:
|
|
175
|
+
raise InvalidParamsError("path required", param="path")
|
|
176
|
+
return await self._bridge_request("open_image", {"path": path}, host, port)
|
|
177
|
+
|
|
178
|
+
@self.action("export", "Export/save an image to a path")
|
|
179
|
+
async def export(
|
|
180
|
+
ctx: MCPContext,
|
|
181
|
+
image_id: int,
|
|
182
|
+
path: str,
|
|
183
|
+
host: str | None = None,
|
|
184
|
+
port: int | None = None,
|
|
185
|
+
) -> dict:
|
|
186
|
+
"""Export an image; the format is chosen by the file extension.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
image_id: GIMP image id.
|
|
190
|
+
path: Destination path (e.g. ``out.png``, ``out.xcf``).
|
|
191
|
+
"""
|
|
192
|
+
if image_id is None:
|
|
193
|
+
raise InvalidParamsError("image_id required", param="image_id")
|
|
194
|
+
if not path:
|
|
195
|
+
raise InvalidParamsError("path required", param="path")
|
|
196
|
+
return await self._bridge_request(
|
|
197
|
+
"export", {"image_id": image_id, "path": path}, host, port
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
@self.action("pdb_call", "Invoke any PDB procedure (generic core power)")
|
|
201
|
+
async def pdb_call(
|
|
202
|
+
ctx: MCPContext,
|
|
203
|
+
procedure: str,
|
|
204
|
+
args: list | None = None,
|
|
205
|
+
host: str | None = None,
|
|
206
|
+
port: int | None = None,
|
|
207
|
+
) -> Any:
|
|
208
|
+
"""Invoke an arbitrary GIMP PDB procedure.
|
|
209
|
+
|
|
210
|
+
Anything GIMP can do is reachable here. Integer ids may be passed
|
|
211
|
+
where a procedure expects a GIMP object (image/drawable/layer/...).
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
procedure: PDB procedure name, e.g. ``gimp-image-flatten``.
|
|
215
|
+
args: Positional arguments in PDB order.
|
|
216
|
+
"""
|
|
217
|
+
if not procedure:
|
|
218
|
+
raise InvalidParamsError("procedure required", param="procedure")
|
|
219
|
+
return await self._bridge_request(
|
|
220
|
+
"pdb_call", {"procedure": procedure, "args": args or []}, host, port
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
@self.action("new_image", "Create a blank RGB image")
|
|
224
|
+
async def new_image(
|
|
225
|
+
ctx: MCPContext,
|
|
226
|
+
width: int,
|
|
227
|
+
height: int,
|
|
228
|
+
host: str | None = None,
|
|
229
|
+
port: int | None = None,
|
|
230
|
+
) -> dict:
|
|
231
|
+
"""Create a new blank RGB image.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
width: Image width in pixels.
|
|
235
|
+
height: Image height in pixels.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
``{image_id, width, height}``.
|
|
239
|
+
"""
|
|
240
|
+
if width is None or height is None:
|
|
241
|
+
raise InvalidParamsError(
|
|
242
|
+
"width and height required", param="width"
|
|
243
|
+
)
|
|
244
|
+
return await self._bridge_request(
|
|
245
|
+
"new_image", {"width": width, "height": height}, host, port
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
@self.action("image_info", "Inspect an image")
|
|
249
|
+
async def image_info(
|
|
250
|
+
ctx: MCPContext,
|
|
251
|
+
image_id: int,
|
|
252
|
+
host: str | None = None,
|
|
253
|
+
port: int | None = None,
|
|
254
|
+
) -> dict:
|
|
255
|
+
"""Return image size, layers, precision, source file, and dirty flag."""
|
|
256
|
+
if image_id is None:
|
|
257
|
+
raise InvalidParamsError("image_id required", param="image_id")
|
|
258
|
+
return await self._bridge_request(
|
|
259
|
+
"image_info", {"image_id": image_id}, host, port
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
@self.action("list_procedures", "Enumerate PDB procedures")
|
|
263
|
+
async def list_procedures(
|
|
264
|
+
ctx: MCPContext,
|
|
265
|
+
prefix: str | None = None,
|
|
266
|
+
host: str | None = None,
|
|
267
|
+
port: int | None = None,
|
|
268
|
+
) -> dict:
|
|
269
|
+
"""List PDB procedure names, optionally filtered by ``prefix``.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
``{count, procedures: [...]}``.
|
|
273
|
+
"""
|
|
274
|
+
params = {"prefix": prefix} if prefix else {}
|
|
275
|
+
return await self._bridge_request(
|
|
276
|
+
"list_procedures", params, host, port
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
@self.action("flatten", "Flatten an image")
|
|
280
|
+
async def flatten(
|
|
281
|
+
ctx: MCPContext,
|
|
282
|
+
image_id: int,
|
|
283
|
+
host: str | None = None,
|
|
284
|
+
port: int | None = None,
|
|
285
|
+
) -> dict:
|
|
286
|
+
"""Flatten all layers of an image into one.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
``{image_id, layer_id}``.
|
|
290
|
+
"""
|
|
291
|
+
if image_id is None:
|
|
292
|
+
raise InvalidParamsError("image_id required", param="image_id")
|
|
293
|
+
return await self._bridge_request(
|
|
294
|
+
"flatten", {"image_id": image_id}, host, port
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# Backward compatibility / convenience handle.
|
|
299
|
+
gimp_tool = GimpTool
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hanzo-tools-gimp"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Unified GIMP automation tool for Hanzo AI (HIP-0300) — clean-room BSD-3 client for the GIMP PDB bridge"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "BSD-3-Clause" }
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Hanzo AI", email = "dev@hanzo.ai" },
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"hanzo",
|
|
17
|
+
"mcp",
|
|
18
|
+
"tools",
|
|
19
|
+
"gimp",
|
|
20
|
+
"pdb",
|
|
21
|
+
"image",
|
|
22
|
+
"automation",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 4 - Beta",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"License :: OSI Approved :: BSD License",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Topic :: Multimedia :: Graphics",
|
|
31
|
+
]
|
|
32
|
+
dependencies = [
|
|
33
|
+
"hanzo-tools>=0.3.0",
|
|
34
|
+
"mcp>=1.0.0",
|
|
35
|
+
"pydantic>=2.0.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
dev = [
|
|
40
|
+
"pytest>=8.0.0",
|
|
41
|
+
"pytest-asyncio>=0.23.0",
|
|
42
|
+
"ruff>=0.1.0",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[project.entry-points."hanzo.tools"]
|
|
46
|
+
gimp = "hanzo_tools.gimp:TOOLS"
|
|
47
|
+
|
|
48
|
+
[project.urls]
|
|
49
|
+
Homepage = "https://github.com/hanzoai/python-sdk"
|
|
50
|
+
Documentation = "https://docs.hanzo.ai"
|
|
51
|
+
Repository = "https://github.com/hanzoai/python-sdk"
|
|
52
|
+
|
|
53
|
+
[tool.hatch.build.targets.wheel]
|
|
54
|
+
packages = ["hanzo_tools"]
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
line-length = 100
|
|
58
|
+
target-version = "py312"
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = ["E", "F", "I", "UP"]
|
|
62
|
+
ignore = ["E501"]
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
asyncio_mode = "auto"
|
|
66
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Tests for hanzo-tools-gimp."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestImports:
|
|
7
|
+
"""Test that all modules can be imported."""
|
|
8
|
+
|
|
9
|
+
def test_import_package(self):
|
|
10
|
+
from hanzo_tools import gimp
|
|
11
|
+
|
|
12
|
+
assert gimp is not None
|
|
13
|
+
|
|
14
|
+
def test_import_tools(self):
|
|
15
|
+
from hanzo_tools.gimp import TOOLS
|
|
16
|
+
|
|
17
|
+
assert len(TOOLS) == 1
|
|
18
|
+
|
|
19
|
+
def test_import_gimp_tool(self):
|
|
20
|
+
from hanzo_tools.gimp import GimpTool
|
|
21
|
+
|
|
22
|
+
assert GimpTool.name == "gimp"
|
|
23
|
+
|
|
24
|
+
def test_register_tools_callable(self):
|
|
25
|
+
from hanzo_tools.gimp import register_tools
|
|
26
|
+
|
|
27
|
+
assert callable(register_tools)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestGimpTool:
|
|
31
|
+
"""Tests for GimpTool."""
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def tool(self):
|
|
35
|
+
from hanzo_tools.gimp import GimpTool
|
|
36
|
+
|
|
37
|
+
return GimpTool()
|
|
38
|
+
|
|
39
|
+
def test_name(self, tool):
|
|
40
|
+
assert tool.name == "gimp"
|
|
41
|
+
|
|
42
|
+
def test_version(self, tool):
|
|
43
|
+
assert tool.VERSION
|
|
44
|
+
|
|
45
|
+
def test_has_description(self, tool):
|
|
46
|
+
assert tool.description
|
|
47
|
+
assert "gimp" in tool.description.lower()
|
|
48
|
+
|
|
49
|
+
def test_default_endpoint(self, tool):
|
|
50
|
+
assert tool.host == "127.0.0.1"
|
|
51
|
+
assert tool.port == 9876
|
|
52
|
+
|
|
53
|
+
def test_custom_endpoint(self):
|
|
54
|
+
from hanzo_tools.gimp import GimpTool
|
|
55
|
+
|
|
56
|
+
t = GimpTool(host="10.0.0.5", port=12345)
|
|
57
|
+
assert t.host == "10.0.0.5"
|
|
58
|
+
assert t.port == 12345
|
|
59
|
+
|
|
60
|
+
def test_action_surface(self, tool):
|
|
61
|
+
# The tool must register exactly the documented actions
|
|
62
|
+
# (plus the BaseTool built-ins: help, schema, status).
|
|
63
|
+
expected = {
|
|
64
|
+
"version",
|
|
65
|
+
"open",
|
|
66
|
+
"export",
|
|
67
|
+
"pdb_call",
|
|
68
|
+
"new_image",
|
|
69
|
+
"image_info",
|
|
70
|
+
"list_procedures",
|
|
71
|
+
"flatten",
|
|
72
|
+
}
|
|
73
|
+
registered = set(tool._handlers.keys())
|
|
74
|
+
assert expected <= registered, expected - registered
|
|
75
|
+
|
|
76
|
+
def test_builtin_actions_present(self, tool):
|
|
77
|
+
registered = set(tool._handlers.keys())
|
|
78
|
+
assert {"help", "schema", "status"} <= registered
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TestSchema:
|
|
82
|
+
"""Schema generation from the registered actions."""
|
|
83
|
+
|
|
84
|
+
@pytest.fixture
|
|
85
|
+
def tool(self):
|
|
86
|
+
from hanzo_tools.gimp import GimpTool
|
|
87
|
+
|
|
88
|
+
return GimpTool()
|
|
89
|
+
|
|
90
|
+
@pytest.mark.asyncio
|
|
91
|
+
async def test_schema_action_returns_schemas(self, tool):
|
|
92
|
+
env = await tool.call(None, "schema")
|
|
93
|
+
assert env["ok"] is True
|
|
94
|
+
schemas = env["data"]["schemas"]
|
|
95
|
+
assert "pdb_call" in schemas
|
|
96
|
+
# pdb_call requires a "procedure" string parameter.
|
|
97
|
+
pdb = schemas["pdb_call"]
|
|
98
|
+
assert pdb["properties"]["procedure"]["type"] == "string"
|
|
99
|
+
assert "procedure" in pdb["required"]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TestBridgeErrors:
|
|
103
|
+
"""Behavioral tests that do not require a running GIMP."""
|
|
104
|
+
|
|
105
|
+
@pytest.fixture
|
|
106
|
+
def tool(self):
|
|
107
|
+
from hanzo_tools.gimp import GimpTool
|
|
108
|
+
|
|
109
|
+
# Point at a closed port so connection fails fast.
|
|
110
|
+
return GimpTool(host="127.0.0.1", port=9)
|
|
111
|
+
|
|
112
|
+
@pytest.mark.asyncio
|
|
113
|
+
async def test_connection_failure_is_reported(self, tool):
|
|
114
|
+
# call() catches ToolError and returns an error envelope.
|
|
115
|
+
env = await tool.call(None, "version")
|
|
116
|
+
assert env["ok"] is False
|
|
117
|
+
assert env["error"] is not None
|
|
118
|
+
|
|
119
|
+
@pytest.mark.asyncio
|
|
120
|
+
async def test_missing_required_param(self, tool):
|
|
121
|
+
# pdb_call without a procedure -> InvalidParamsError -> error envelope.
|
|
122
|
+
env = await tool.call(None, "pdb_call")
|
|
123
|
+
assert env["ok"] is False
|