mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
feat(tools): hanzo-tools-gimp (clean-room BSD-3)
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
# hanzo-tools-gimp
|
||||
|
||||
Unified **GIMP automation** tool for Hanzo AI (HIP-0300).
|
||||
|
||||
Drives [GIMP](https://www.gimp.org/) 3.0 through the **Hanzo GIMP bridge** — a
|
||||
clean-room, **BSD-3** GIMP plug-in (see `hanzo/gimp-mcp`) that exposes GIMP's
|
||||
Procedural Database (PDB) over a line-oriented JSON TCP socket. This package is
|
||||
the Python client for that bridge.
|
||||
|
||||
This is a clean-room implementation with **no GPL lineage**: both the bridge
|
||||
and this client use GIMP's documented, public PDB / GObject-Introspection API.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
pip install hanzo-tools-gimp
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The Hanzo GIMP bridge plug-in must be running inside GIMP. Briefly:
|
||||
|
||||
1. Copy `hanzo_gimp_bridge.py` (from `hanzo/gimp-mcp`) into your GIMP 3.0
|
||||
plug-ins directory (one folder per plug-in, script made executable).
|
||||
2. Start GIMP and run **Filters → Hanzo → Start Hanzo Bridge**.
|
||||
3. The bridge listens on `127.0.0.1:9876` by default (override with
|
||||
`HANZO_GIMP_PORT` / `HANZO_GIMP_HOST`).
|
||||
|
||||
See the `gimp-mcp` repo's `README.md` and `PROTOCOL.md` for full details.
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
from hanzo_tools.gimp import GimpTool, register_tools, TOOLS
|
||||
|
||||
# Register with a FastMCP server
|
||||
register_tools(mcp_server) # optional host=..., port=...
|
||||
|
||||
# Or use the tool directly
|
||||
tool = GimpTool(host="127.0.0.1", port=9876)
|
||||
```
|
||||
|
||||
The tool exposes the same action surface as the Hanzo MCP TypeScript `gimp`
|
||||
tool:
|
||||
|
||||
| Action | Params | Result |
|
||||
|-------------------|---------------------------------|--------|
|
||||
| `version` | — | `{bridge, gimp, protocol}` |
|
||||
| `open` | `path` | `{image_id, width, height}` |
|
||||
| `export` | `image_id`, `path` | `{image_id, path, saved}` |
|
||||
| `pdb_call` | `procedure`, `args` | the procedure's return value(s) |
|
||||
| `new_image` | `width`, `height` | `{image_id, width, height}` |
|
||||
| `image_info` | `image_id` | `{image_id, width, height, ...}` |
|
||||
| `list_procedures` | `prefix?` | `{count, procedures}` |
|
||||
| `flatten` | `image_id` | `{image_id, layer_id}` |
|
||||
|
||||
Every action also accepts `host` / `port` overrides.
|
||||
|
||||
`pdb_call` is the generic escape hatch — it can invoke **any** PDB procedure,
|
||||
so anything GIMP can do is reachable.
|
||||
|
||||
## License
|
||||
|
||||
**BSD-3-Clause**. Copyright (c) 2026 Hanzo AI.
|
||||
@@ -0,0 +1,4 @@
|
||||
# Namespace package
|
||||
from pkgutil import extend_path
|
||||
|
||||
__path__ = extend_path(__path__, __name__)
|
||||
@@ -0,0 +1,64 @@
|
||||
"""GIMP automation tools for Hanzo AI (HIP-0300).
|
||||
|
||||
Drives GIMP 3.0 through the Hanzo GIMP bridge -- a clean-room, BSD-3 GIMP
|
||||
plug-in that exposes GIMP's Procedural Database (PDB) over a JSON TCP socket.
|
||||
No GPL lineage.
|
||||
|
||||
Tools:
|
||||
- gimp: Unified GIMP automation tool (HIP-0300)
|
||||
- version: Bridge + GIMP version
|
||||
- open: Load an image file
|
||||
- export: Export/save an image
|
||||
- pdb_call: Invoke any PDB procedure (the core power)
|
||||
- new_image: Create a blank RGB image
|
||||
- image_info: Inspect an image
|
||||
- list_procedures: Enumerate PDB procedures
|
||||
- flatten: Flatten an image
|
||||
|
||||
Install:
|
||||
pip install hanzo-tools-gimp
|
||||
|
||||
Usage:
|
||||
from hanzo_tools.gimp import register_tools, TOOLS, GimpTool
|
||||
|
||||
# Register with an MCP server
|
||||
register_tools(mcp_server)
|
||||
|
||||
# Or use the unified tool directly
|
||||
tool = GimpTool(host="127.0.0.1", port=9876)
|
||||
|
||||
Requires the Hanzo GIMP bridge plug-in (hanzo/gimp-mcp) running inside GIMP.
|
||||
"""
|
||||
|
||||
from hanzo_tools.core import BaseTool, ToolRegistry
|
||||
|
||||
from .gimp_tool import DEFAULT_HOST, DEFAULT_PORT, GimpTool, gimp_tool
|
||||
|
||||
# Export list for tool discovery - HIP-0300 unified tool
|
||||
TOOLS = [GimpTool]
|
||||
|
||||
__all__ = [
|
||||
"GimpTool",
|
||||
"gimp_tool",
|
||||
"DEFAULT_HOST",
|
||||
"DEFAULT_PORT",
|
||||
"register_tools",
|
||||
"TOOLS",
|
||||
]
|
||||
|
||||
|
||||
def register_tools(mcp_server, **kwargs) -> list[BaseTool]:
|
||||
"""Register gimp tools with the MCP server.
|
||||
|
||||
Args:
|
||||
mcp_server: The FastMCP server instance.
|
||||
**kwargs: Optional ``host`` / ``port`` for the GIMP bridge.
|
||||
|
||||
Returns:
|
||||
List of registered tool instances.
|
||||
"""
|
||||
host = kwargs.get("host")
|
||||
port = kwargs.get("port")
|
||||
tool = GimpTool(host=host, port=port)
|
||||
ToolRegistry.register_tool(mcp_server, tool)
|
||||
return [tool]
|
||||
@@ -0,0 +1,299 @@
|
||||
"""Unified GIMP automation tool for the HIP-0300 architecture.
|
||||
|
||||
This module provides a single unified ``gimp`` tool that drives GIMP 3.0
|
||||
through the Hanzo GIMP bridge -- a clean-room, BSD-3 GIMP plug-in that exposes
|
||||
GIMP's Procedural Database (PDB) over a line-oriented JSON TCP socket.
|
||||
|
||||
Actions (matching the TypeScript ``gimp`` tool):
|
||||
- version: bridge + GIMP version
|
||||
- open: load an image file
|
||||
- export: export/save an image to a path
|
||||
- pdb_call: invoke any PDB procedure (the core power)
|
||||
- new_image: create a blank RGB image
|
||||
- image_info: inspect an image
|
||||
- list_procedures: enumerate PDB procedures (optional prefix filter)
|
||||
- flatten: flatten an image
|
||||
|
||||
Following Unix philosophy: one tool for the GIMP-automation axis. No GPL
|
||||
lineage -- the bridge and this client speak GIMP's documented public API.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from mcp.server.fastmcp import Context as MCPContext
|
||||
|
||||
from hanzo_tools.core import (
|
||||
BaseTool,
|
||||
InvalidParamsError,
|
||||
ToolError,
|
||||
)
|
||||
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
DEFAULT_PORT = 9876
|
||||
|
||||
|
||||
class GimpTool(BaseTool):
|
||||
"""Unified GIMP automation tool (HIP-0300).
|
||||
|
||||
Connects to the Hanzo GIMP bridge over TCP and drives GIMP through its
|
||||
PDB. The bridge default endpoint is ``127.0.0.1:9876``; every action
|
||||
accepts ``host`` / ``port`` overrides.
|
||||
|
||||
Actions: version, open, export, pdb_call, new_image, image_info,
|
||||
list_procedures, flatten.
|
||||
"""
|
||||
|
||||
name: ClassVar[str] = "gimp"
|
||||
VERSION: ClassVar[str] = "1.0.0"
|
||||
|
||||
def __init__(self, host: str | None = None, port: int | None = None):
|
||||
super().__init__()
|
||||
self.host = host or os.environ.get("HANZO_GIMP_HOST", DEFAULT_HOST)
|
||||
env_port = os.environ.get("HANZO_GIMP_PORT")
|
||||
self.port = port or (int(env_port) if env_port else DEFAULT_PORT)
|
||||
self._register_gimp_actions()
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return """Unified GIMP automation tool (HIP-0300).
|
||||
|
||||
Drives GIMP 3.0 via the clean-room BSD-3 bridge (JSON-over-TCP to the GIMP PDB).
|
||||
|
||||
Actions:
|
||||
- version: Bridge and GIMP version
|
||||
- open: Load an image file (returns image_id, width, height)
|
||||
- export: Export/save an image to a path
|
||||
- pdb_call: Invoke any PDB procedure -- the generic core power
|
||||
- new_image: Create a blank RGB image
|
||||
- image_info: Inspect an image (size, layers, precision, file, dirty)
|
||||
- list_procedures: Enumerate PDB procedures (optional prefix filter)
|
||||
- flatten: Flatten an image
|
||||
|
||||
Requires the Hanzo GIMP bridge plug-in running inside GIMP
|
||||
(default 127.0.0.1:9876).
|
||||
"""
|
||||
|
||||
async def _bridge_request(
|
||||
self,
|
||||
method: str,
|
||||
params: dict[str, Any],
|
||||
host: str | None = None,
|
||||
port: int | None = None,
|
||||
timeout: float = 60.0,
|
||||
) -> Any:
|
||||
"""Send one JSON request to the GIMP bridge and return its result.
|
||||
|
||||
Opens a short-lived connection, writes one newline-terminated JSON
|
||||
request, reads one newline-terminated JSON reply.
|
||||
|
||||
Raises:
|
||||
ToolError: on connection failure, timeout, malformed reply, or an
|
||||
error response from the bridge.
|
||||
"""
|
||||
host = host or self.host
|
||||
port = port or self.port
|
||||
request = json.dumps({"id": 1, "method": method, "params": params}) + "\n"
|
||||
|
||||
try:
|
||||
reader, writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(host, port), timeout=timeout
|
||||
)
|
||||
except (OSError, asyncio.TimeoutError) as exc:
|
||||
raise ToolError(
|
||||
code="NOT_FOUND",
|
||||
message=f"GIMP bridge connection failed ({host}:{port}): {exc}",
|
||||
details={"host": host, "port": port},
|
||||
)
|
||||
|
||||
try:
|
||||
writer.write(request.encode("utf-8"))
|
||||
await writer.drain()
|
||||
line = await asyncio.wait_for(reader.readline(), timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
raise ToolError(code="TIMEOUT", message="GIMP bridge timed out")
|
||||
finally:
|
||||
writer.close()
|
||||
try:
|
||||
await writer.wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not line:
|
||||
raise ToolError(
|
||||
code="INTERNAL_ERROR", message="GIMP bridge closed without replying"
|
||||
)
|
||||
|
||||
try:
|
||||
msg = json.loads(line.decode("utf-8"))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ToolError(
|
||||
code="INTERNAL_ERROR",
|
||||
message=f"Invalid response from GIMP bridge: {exc}",
|
||||
)
|
||||
|
||||
if isinstance(msg, dict) and msg.get("error"):
|
||||
err = msg["error"]
|
||||
raise ToolError(
|
||||
code="INTERNAL_ERROR",
|
||||
message=err.get("message", "GIMP bridge error"),
|
||||
details={"type": err.get("type")},
|
||||
)
|
||||
|
||||
return msg.get("result") if isinstance(msg, dict) else msg
|
||||
|
||||
def _register_gimp_actions(self):
|
||||
"""Register all GIMP actions."""
|
||||
|
||||
@self.action("version", "Get bridge and GIMP version")
|
||||
async def version(
|
||||
ctx: MCPContext,
|
||||
host: str | None = None,
|
||||
port: int | None = None,
|
||||
) -> dict:
|
||||
"""Return the bridge version, GIMP version, and protocol id."""
|
||||
return await self._bridge_request("version", {}, host, port)
|
||||
|
||||
@self.action("open", "Open (load) an image file")
|
||||
async def open_image(
|
||||
ctx: MCPContext,
|
||||
path: str,
|
||||
host: str | None = None,
|
||||
port: int | None = None,
|
||||
) -> dict:
|
||||
"""Load an image from disk.
|
||||
|
||||
Args:
|
||||
path: Filesystem path to the image.
|
||||
|
||||
Returns:
|
||||
``{image_id, width, height}``.
|
||||
"""
|
||||
if not path:
|
||||
raise InvalidParamsError("path required", param="path")
|
||||
return await self._bridge_request("open_image", {"path": path}, host, port)
|
||||
|
||||
@self.action("export", "Export/save an image to a path")
|
||||
async def export(
|
||||
ctx: MCPContext,
|
||||
image_id: int,
|
||||
path: str,
|
||||
host: str | None = None,
|
||||
port: int | None = None,
|
||||
) -> dict:
|
||||
"""Export an image; the format is chosen by the file extension.
|
||||
|
||||
Args:
|
||||
image_id: GIMP image id.
|
||||
path: Destination path (e.g. ``out.png``, ``out.xcf``).
|
||||
"""
|
||||
if image_id is None:
|
||||
raise InvalidParamsError("image_id required", param="image_id")
|
||||
if not path:
|
||||
raise InvalidParamsError("path required", param="path")
|
||||
return await self._bridge_request(
|
||||
"export", {"image_id": image_id, "path": path}, host, port
|
||||
)
|
||||
|
||||
@self.action("pdb_call", "Invoke any PDB procedure (generic core power)")
|
||||
async def pdb_call(
|
||||
ctx: MCPContext,
|
||||
procedure: str,
|
||||
args: list | None = None,
|
||||
host: str | None = None,
|
||||
port: int | None = None,
|
||||
) -> Any:
|
||||
"""Invoke an arbitrary GIMP PDB procedure.
|
||||
|
||||
Anything GIMP can do is reachable here. Integer ids may be passed
|
||||
where a procedure expects a GIMP object (image/drawable/layer/...).
|
||||
|
||||
Args:
|
||||
procedure: PDB procedure name, e.g. ``gimp-image-flatten``.
|
||||
args: Positional arguments in PDB order.
|
||||
"""
|
||||
if not procedure:
|
||||
raise InvalidParamsError("procedure required", param="procedure")
|
||||
return await self._bridge_request(
|
||||
"pdb_call", {"procedure": procedure, "args": args or []}, host, port
|
||||
)
|
||||
|
||||
@self.action("new_image", "Create a blank RGB image")
|
||||
async def new_image(
|
||||
ctx: MCPContext,
|
||||
width: int,
|
||||
height: int,
|
||||
host: str | None = None,
|
||||
port: int | None = None,
|
||||
) -> dict:
|
||||
"""Create a new blank RGB image.
|
||||
|
||||
Args:
|
||||
width: Image width in pixels.
|
||||
height: Image height in pixels.
|
||||
|
||||
Returns:
|
||||
``{image_id, width, height}``.
|
||||
"""
|
||||
if width is None or height is None:
|
||||
raise InvalidParamsError(
|
||||
"width and height required", param="width"
|
||||
)
|
||||
return await self._bridge_request(
|
||||
"new_image", {"width": width, "height": height}, host, port
|
||||
)
|
||||
|
||||
@self.action("image_info", "Inspect an image")
|
||||
async def image_info(
|
||||
ctx: MCPContext,
|
||||
image_id: int,
|
||||
host: str | None = None,
|
||||
port: int | None = None,
|
||||
) -> dict:
|
||||
"""Return image size, layers, precision, source file, and dirty flag."""
|
||||
if image_id is None:
|
||||
raise InvalidParamsError("image_id required", param="image_id")
|
||||
return await self._bridge_request(
|
||||
"image_info", {"image_id": image_id}, host, port
|
||||
)
|
||||
|
||||
@self.action("list_procedures", "Enumerate PDB procedures")
|
||||
async def list_procedures(
|
||||
ctx: MCPContext,
|
||||
prefix: str | None = None,
|
||||
host: str | None = None,
|
||||
port: int | None = None,
|
||||
) -> dict:
|
||||
"""List PDB procedure names, optionally filtered by ``prefix``.
|
||||
|
||||
Returns:
|
||||
``{count, procedures: [...]}``.
|
||||
"""
|
||||
params = {"prefix": prefix} if prefix else {}
|
||||
return await self._bridge_request(
|
||||
"list_procedures", params, host, port
|
||||
)
|
||||
|
||||
@self.action("flatten", "Flatten an image")
|
||||
async def flatten(
|
||||
ctx: MCPContext,
|
||||
image_id: int,
|
||||
host: str | None = None,
|
||||
port: int | None = None,
|
||||
) -> dict:
|
||||
"""Flatten all layers of an image into one.
|
||||
|
||||
Returns:
|
||||
``{image_id, layer_id}``.
|
||||
"""
|
||||
if image_id is None:
|
||||
raise InvalidParamsError("image_id required", param="image_id")
|
||||
return await self._bridge_request(
|
||||
"flatten", {"image_id": image_id}, host, port
|
||||
)
|
||||
|
||||
|
||||
# Backward compatibility / convenience handle.
|
||||
gimp_tool = GimpTool
|
||||
@@ -0,0 +1,66 @@
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "hanzo-tools-gimp"
|
||||
version = "1.0.0"
|
||||
description = "Unified GIMP automation tool for Hanzo AI (HIP-0300) — clean-room BSD-3 client for the GIMP PDB bridge"
|
||||
readme = "README.md"
|
||||
license = { text = "BSD-3-Clause" }
|
||||
requires-python = ">=3.12"
|
||||
authors = [
|
||||
{ name = "Hanzo AI", email = "[email protected]" },
|
||||
]
|
||||
keywords = [
|
||||
"hanzo",
|
||||
"mcp",
|
||||
"tools",
|
||||
"gimp",
|
||||
"pdb",
|
||||
"image",
|
||||
"automation",
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: BSD License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Multimedia :: Graphics",
|
||||
]
|
||||
dependencies = [
|
||||
"hanzo-tools>=0.3.0",
|
||||
"mcp>=1.0.0",
|
||||
"pydantic>=2.0.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=8.0.0",
|
||||
"pytest-asyncio>=0.23.0",
|
||||
"ruff>=0.1.0",
|
||||
]
|
||||
|
||||
[project.entry-points."hanzo.tools"]
|
||||
gimp = "hanzo_tools.gimp:TOOLS"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/hanzoai/python-sdk"
|
||||
Documentation = "https://docs.hanzo.ai"
|
||||
Repository = "https://github.com/hanzoai/python-sdk"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["hanzo_tools"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py312"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "UP"]
|
||||
ignore = ["E501"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
testpaths = ["tests"]
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Tests for hanzo-tools-gimp."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestImports:
|
||||
"""Test that all modules can be imported."""
|
||||
|
||||
def test_import_package(self):
|
||||
from hanzo_tools import gimp
|
||||
|
||||
assert gimp is not None
|
||||
|
||||
def test_import_tools(self):
|
||||
from hanzo_tools.gimp import TOOLS
|
||||
|
||||
assert len(TOOLS) == 1
|
||||
|
||||
def test_import_gimp_tool(self):
|
||||
from hanzo_tools.gimp import GimpTool
|
||||
|
||||
assert GimpTool.name == "gimp"
|
||||
|
||||
def test_register_tools_callable(self):
|
||||
from hanzo_tools.gimp import register_tools
|
||||
|
||||
assert callable(register_tools)
|
||||
|
||||
|
||||
class TestGimpTool:
|
||||
"""Tests for GimpTool."""
|
||||
|
||||
@pytest.fixture
|
||||
def tool(self):
|
||||
from hanzo_tools.gimp import GimpTool
|
||||
|
||||
return GimpTool()
|
||||
|
||||
def test_name(self, tool):
|
||||
assert tool.name == "gimp"
|
||||
|
||||
def test_version(self, tool):
|
||||
assert tool.VERSION
|
||||
|
||||
def test_has_description(self, tool):
|
||||
assert tool.description
|
||||
assert "gimp" in tool.description.lower()
|
||||
|
||||
def test_default_endpoint(self, tool):
|
||||
assert tool.host == "127.0.0.1"
|
||||
assert tool.port == 9876
|
||||
|
||||
def test_custom_endpoint(self):
|
||||
from hanzo_tools.gimp import GimpTool
|
||||
|
||||
t = GimpTool(host="10.0.0.5", port=12345)
|
||||
assert t.host == "10.0.0.5"
|
||||
assert t.port == 12345
|
||||
|
||||
def test_action_surface(self, tool):
|
||||
# The tool must register exactly the documented actions
|
||||
# (plus the BaseTool built-ins: help, schema, status).
|
||||
expected = {
|
||||
"version",
|
||||
"open",
|
||||
"export",
|
||||
"pdb_call",
|
||||
"new_image",
|
||||
"image_info",
|
||||
"list_procedures",
|
||||
"flatten",
|
||||
}
|
||||
registered = set(tool._handlers.keys())
|
||||
assert expected <= registered, expected - registered
|
||||
|
||||
def test_builtin_actions_present(self, tool):
|
||||
registered = set(tool._handlers.keys())
|
||||
assert {"help", "schema", "status"} <= registered
|
||||
|
||||
|
||||
class TestSchema:
|
||||
"""Schema generation from the registered actions."""
|
||||
|
||||
@pytest.fixture
|
||||
def tool(self):
|
||||
from hanzo_tools.gimp import GimpTool
|
||||
|
||||
return GimpTool()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_schema_action_returns_schemas(self, tool):
|
||||
env = await tool.call(None, "schema")
|
||||
assert env["ok"] is True
|
||||
schemas = env["data"]["schemas"]
|
||||
assert "pdb_call" in schemas
|
||||
# pdb_call requires a "procedure" string parameter.
|
||||
pdb = schemas["pdb_call"]
|
||||
assert pdb["properties"]["procedure"]["type"] == "string"
|
||||
assert "procedure" in pdb["required"]
|
||||
|
||||
|
||||
class TestBridgeErrors:
|
||||
"""Behavioral tests that do not require a running GIMP."""
|
||||
|
||||
@pytest.fixture
|
||||
def tool(self):
|
||||
from hanzo_tools.gimp import GimpTool
|
||||
|
||||
# Point at a closed port so connection fails fast.
|
||||
return GimpTool(host="127.0.0.1", port=9)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connection_failure_is_reported(self, tool):
|
||||
# call() catches ToolError and returns an error envelope.
|
||||
env = await tool.call(None, "version")
|
||||
assert env["ok"] is False
|
||||
assert env["error"] is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_required_param(self, tool):
|
||||
# pdb_call without a procedure -> InvalidParamsError -> error envelope.
|
||||
env = await tool.call(None, "pdb_call")
|
||||
assert env["ok"] is False
|
||||
Reference in New Issue
Block a user