rossum-mcp 0.3.4__py3-none-any.whl

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.
rossum_mcp/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from __future__ import annotations
2
+
3
+ __version__ = "0.3.4"
@@ -0,0 +1,141 @@
1
+ """Shared logging configuration for Rossum MCP/Agent with Redis integration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ import sys
9
+ from datetime import UTC, datetime
10
+ from logging import LogRecord
11
+
12
+ import redis
13
+
14
+
15
+ class RedisHandler(logging.Handler):
16
+ """Custom logging handler that sends logs to Redis."""
17
+
18
+ def __init__(self, host: str, port: int = 6379, key_prefix: str = "logs", additional_fields: dict | None = None):
19
+ """Initialize Redis handler.
20
+
21
+ Args:
22
+ host: Redis host (e.g., "localhost")
23
+ port: Redis port (default: 6379)
24
+ key_prefix: Prefix for Redis keys
25
+ additional_fields: Additional fields to add to every log record
26
+ """
27
+ super().__init__()
28
+
29
+ self.client = redis.Redis(host=host, port=port, decode_responses=True)
30
+ self.key_prefix = key_prefix
31
+ self.additional_fields = additional_fields or {}
32
+
33
+ def emit(self, record: LogRecord) -> None:
34
+ """Emit a log record to Redis."""
35
+ if record.name.startswith("redis"):
36
+ return
37
+
38
+ try:
39
+ log_entry = {
40
+ "@timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
41
+ "level": record.levelname,
42
+ "logger": record.name,
43
+ "message": record.getMessage(),
44
+ "module": record.module,
45
+ "function": record.funcName,
46
+ "line": record.lineno,
47
+ "thread": record.thread,
48
+ "thread_name": record.threadName,
49
+ **self.additional_fields,
50
+ }
51
+
52
+ for key, value in record.__dict__.items():
53
+ if key not in {
54
+ "name",
55
+ "msg",
56
+ "args",
57
+ "created",
58
+ "filename",
59
+ "funcName",
60
+ "levelname",
61
+ "levelno",
62
+ "lineno",
63
+ "module",
64
+ "msecs",
65
+ "message",
66
+ "pathname",
67
+ "process",
68
+ "processName",
69
+ "relativeCreated",
70
+ "thread",
71
+ "threadName",
72
+ "exc_info",
73
+ "exc_text",
74
+ "stack_info",
75
+ "taskName",
76
+ }:
77
+ log_entry[key] = value
78
+
79
+ if record.exc_info:
80
+ log_entry["exception"] = self.format(record)
81
+
82
+ key = f"{self.key_prefix}:{datetime.now(UTC).strftime('%Y-%m-%d')}"
83
+ self.client.rpush(key, json.dumps(log_entry))
84
+ self.client.expire(key, 604800) # 7 days
85
+ except Exception:
86
+ self.handleError(record)
87
+
88
+
89
+ def setup_logging(
90
+ app_name: str = "rossum-app",
91
+ log_level: str = "DEBUG",
92
+ log_file: str | None = None,
93
+ use_console: bool = True,
94
+ redis_host: str | None = None,
95
+ redis_port: int | None = None,
96
+ ) -> logging.Logger:
97
+ """Configure logging with console, file, and optional Redis handlers.
98
+
99
+ Args:
100
+ app_name: Application name
101
+ log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
102
+ use_console: Whether to add console handler (default: True)
103
+ redis_host: Redis host (default: from REDIS_HOST env var)
104
+ redis_port: Redis port (default: 6379)
105
+
106
+ Returns:
107
+ Configured root logger
108
+ """
109
+ root_logger = logging.getLogger()
110
+ root_logger.setLevel(getattr(logging, log_level.upper()))
111
+ root_logger.handlers.clear()
112
+
113
+ formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
114
+
115
+ if use_console:
116
+ console = logging.StreamHandler(sys.stdout)
117
+ console.setFormatter(formatter)
118
+ root_logger.addHandler(console)
119
+
120
+ redis_host_val = redis_host or os.getenv("REDIS_HOST")
121
+ redis_port_val = redis_port or int(os.getenv("REDIS_PORT", "6379"))
122
+
123
+ if redis_host_val:
124
+ try:
125
+ redis_client = redis.Redis(host=redis_host_val, port=redis_port_val, socket_timeout=2)
126
+ redis_client.ping()
127
+
128
+ redis_handler = RedisHandler(
129
+ host=redis_host_val,
130
+ port=redis_port_val,
131
+ key_prefix="logs",
132
+ additional_fields={"application": app_name, "environment": os.getenv("ENVIRONMENT", "develop")},
133
+ )
134
+ redis_handler.client = redis_client
135
+ redis_handler.setLevel(logging.INFO)
136
+ root_logger.addHandler(redis_handler)
137
+ root_logger.info(f"Redis logging enabled: {redis_host_val}:{redis_port_val}")
138
+ except Exception as e:
139
+ root_logger.warning(f"Redis logging disabled: {e}")
140
+
141
+ return root_logger
rossum_mcp/py.typed ADDED
File without changes
rossum_mcp/server.py ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env python3
2
+ """Rossum MCP Server
3
+
4
+ Provides tools for uploading documents and retrieving annotations using Rossum API.
5
+ Built with FastMCP for a cleaner, more Pythonic interface.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import os
12
+
13
+ from fastmcp import FastMCP
14
+ from rossum_api import AsyncRossumAPIClient
15
+ from rossum_api.dtos import Token
16
+
17
+ from rossum_mcp.logging_config import setup_logging
18
+ from rossum_mcp.tools import (
19
+ register_annotation_tools,
20
+ register_document_relation_tools,
21
+ register_engine_tools,
22
+ register_hook_tools,
23
+ register_queue_tools,
24
+ register_relation_tools,
25
+ register_rule_tools,
26
+ register_schema_tools,
27
+ register_user_tools,
28
+ register_workspace_tools,
29
+ )
30
+
31
+ setup_logging(app_name="rossum-mcp-server", log_level="DEBUG", use_console=False)
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ BASE_URL = os.environ["ROSSUM_API_BASE_URL"].rstrip("/")
36
+ API_TOKEN = os.environ["ROSSUM_API_TOKEN"]
37
+ MODE = os.environ.get("ROSSUM_MCP_MODE", "read-write").lower()
38
+
39
+ if MODE not in ("read-only", "read-write"):
40
+ raise ValueError(f"Invalid ROSSUM_MCP_MODE: {MODE}. Must be 'read-only' or 'read-write'")
41
+
42
+ logger.info(f"Rossum MCP Server starting in {MODE} mode")
43
+
44
+ mcp = FastMCP("rossum-mcp-server")
45
+ client = AsyncRossumAPIClient(base_url=BASE_URL, credentials=Token(token=API_TOKEN))
46
+
47
+ register_annotation_tools(mcp, client)
48
+ register_queue_tools(mcp, client)
49
+ register_schema_tools(mcp, client)
50
+ register_engine_tools(mcp, client)
51
+ register_hook_tools(mcp, client)
52
+ register_document_relation_tools(mcp, client)
53
+ register_relation_tools(mcp, client)
54
+ register_rule_tools(mcp, client)
55
+ register_user_tools(mcp, client)
56
+ register_workspace_tools(mcp, client)
57
+
58
+
59
+ def main() -> None:
60
+ """Main entry point for console script."""
61
+ mcp.run()
62
+
63
+
64
+ if __name__ == "__main__":
65
+ main()
@@ -0,0 +1,27 @@
1
+ """FastMCP tool modules for Rossum MCP Server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rossum_mcp.tools.annotations import register_annotation_tools
6
+ from rossum_mcp.tools.document_relations import register_document_relation_tools
7
+ from rossum_mcp.tools.engines import register_engine_tools
8
+ from rossum_mcp.tools.hooks import register_hook_tools
9
+ from rossum_mcp.tools.queues import register_queue_tools
10
+ from rossum_mcp.tools.relations import register_relation_tools
11
+ from rossum_mcp.tools.rules import register_rule_tools
12
+ from rossum_mcp.tools.schemas import register_schema_tools
13
+ from rossum_mcp.tools.users import register_user_tools
14
+ from rossum_mcp.tools.workspaces import register_workspace_tools
15
+
16
+ __all__ = [
17
+ "register_annotation_tools",
18
+ "register_document_relation_tools",
19
+ "register_engine_tools",
20
+ "register_hook_tools",
21
+ "register_queue_tools",
22
+ "register_relation_tools",
23
+ "register_rule_tools",
24
+ "register_schema_tools",
25
+ "register_user_tools",
26
+ "register_workspace_tools",
27
+ ]
@@ -0,0 +1,130 @@
1
+ """Annotation tools for Rossum MCP Server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Sequence # noqa: TC003 - needed at runtime for FastMCP
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING, Literal
9
+
10
+ from rossum_api.models.annotation import Annotation # noqa: TC002 - needed at runtime for FastMCP
11
+
12
+ from rossum_mcp.tools.base import is_read_write_mode
13
+
14
+ if TYPE_CHECKING:
15
+ from fastmcp import FastMCP
16
+ from rossum_api import AsyncRossumAPIClient
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # Fixed sideloads (critical for well-behaving agent)
21
+ type Sideload = Literal["content", "document", "automation_blocker"]
22
+
23
+
24
+ def register_annotation_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None: # noqa: C901
25
+ """Register annotation-related tools with the FastMCP server."""
26
+
27
+ @mcp.tool(description="Upload a document to Rossum. Use list_annotations to get annotation ID.")
28
+ async def upload_document(file_path: str, queue_id: int) -> dict:
29
+ """Upload a document to Rossum."""
30
+ if not is_read_write_mode():
31
+ return {"error": "upload_document is not available in read-only mode"}
32
+
33
+ path = Path(file_path)
34
+ if not path.exists():
35
+ logger.error(f"File not found: {file_path}")
36
+ raise FileNotFoundError(f"File not found: {file_path}")
37
+
38
+ try:
39
+ task = (await client.upload_document(queue_id, [(str(path), path.name)]))[0]
40
+ except KeyError as e:
41
+ logger.error(f"Upload failed - unexpected API response format: {e!s}")
42
+ error_msg = (
43
+ f"Document upload failed - API response missing expected key {e!s}. "
44
+ f"This usually means either:\n"
45
+ f"1. The queue_id ({queue_id}) is invalid or you don't have access to it\n"
46
+ f"2. The Rossum API returned an error response\n"
47
+ f"Please verify:\n"
48
+ f"- The queue_id is correct and exists in your workspace\n"
49
+ f"- You have permission to upload documents to this queue\n"
50
+ f"- Your API token has the necessary permissions"
51
+ )
52
+ raise ValueError(error_msg) from e
53
+ except IndexError as e:
54
+ logger.error(f"Upload failed - no tasks returned: {e}")
55
+ raise ValueError(
56
+ f"Document upload failed - no tasks were created. This may indicate the queue_id ({queue_id}) is invalid."
57
+ ) from e
58
+ except Exception as e:
59
+ logger.error(f"Upload failed: {type(e).__name__}: {e}")
60
+ raise ValueError(f"Document upload failed: {type(e).__name__}: {e!s}") from e
61
+
62
+ return {
63
+ "task_id": task.id,
64
+ "task_status": task.status,
65
+ "queue_id": queue_id,
66
+ "message": "Document upload initiated. Use `list_annotations` to find the annotation ID for this queue.",
67
+ }
68
+
69
+ @mcp.tool(description="Retrieve annotation data. Use 'content' sideload to get extracted data.")
70
+ async def get_annotation(annotation_id: int, sideloads: Sequence[Sideload] = ()) -> Annotation:
71
+ """Retrieve annotation data from Rossum."""
72
+ logger.debug(f"Retrieving annotation: annotation_id={annotation_id}")
73
+ annotation: Annotation = await client.retrieve_annotation(annotation_id, sideloads) # type: ignore[arg-type]
74
+ return annotation
75
+
76
+ @mcp.tool(description="List annotations for a queue.")
77
+ async def list_annotations(
78
+ queue_id: int, status: str | None = "importing,to_review,confirmed,exported"
79
+ ) -> list[Annotation]:
80
+ """List annotations for a queue with optional filtering."""
81
+ logger.debug(f"Listing annotations: queue_id={queue_id}, status={status}")
82
+ params: dict = {"queue": queue_id, "page_size": 100}
83
+ if status:
84
+ params["status"] = status
85
+ annotations_list: list[Annotation] = [item async for item in client.list_annotations(**params)]
86
+ return annotations_list
87
+
88
+ @mcp.tool(description="Start annotation (move from 'to_review' to 'reviewing').")
89
+ async def start_annotation(annotation_id: int) -> dict:
90
+ """Start annotation to move it to 'reviewing' status."""
91
+ if not is_read_write_mode():
92
+ return {"error": "start_annotation is not available in read-only mode"}
93
+
94
+ logger.debug(f"Starting annotation: annotation_id={annotation_id}")
95
+ await client.start_annotation(annotation_id)
96
+ return {
97
+ "annotation_id": annotation_id,
98
+ "message": f"Annotation {annotation_id} started successfully. Status changed to 'reviewing'.",
99
+ }
100
+
101
+ @mcp.tool(
102
+ description="Bulk update annotation fields. It can be used after `start_annotation` only. Use datapoint ID from content, NOT schema_id."
103
+ )
104
+ async def bulk_update_annotation_fields(annotation_id: int, operations: list[dict]) -> dict:
105
+ """Bulk update annotation field values using operations."""
106
+ if not is_read_write_mode():
107
+ return {"error": "bulk_update_annotation_fields is not available in read-only mode"}
108
+
109
+ logger.debug(f"Bulk updating annotation: annotation_id={annotation_id}, ops={operations}")
110
+ await client.bulk_update_annotation_data(annotation_id, operations)
111
+ return {
112
+ "annotation_id": annotation_id,
113
+ "operations_count": len(operations),
114
+ "message": f"Annotation {annotation_id} updated with {len(operations)} operations successfully.",
115
+ }
116
+
117
+ @mcp.tool(
118
+ description="Confirm annotation (move to 'confirmed'). It can be used after `bulk_update_annotation_fields`."
119
+ )
120
+ async def confirm_annotation(annotation_id: int) -> dict:
121
+ """Confirm annotation to move it to 'confirmed' status."""
122
+ if not is_read_write_mode():
123
+ return {"error": "confirm_annotation is not available in read-only mode"}
124
+
125
+ logger.debug(f"Confirming annotation: annotation_id={annotation_id}")
126
+ await client.confirm_annotation(annotation_id)
127
+ return {
128
+ "annotation_id": annotation_id,
129
+ "message": f"Annotation {annotation_id} confirmed successfully. Status changed to 'confirmed'.",
130
+ }
@@ -0,0 +1,18 @@
1
+ """Base utilities for Rossum MCP tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ BASE_URL = os.environ.get("ROSSUM_API_BASE_URL", "").rstrip("/")
8
+ MODE = os.environ.get("ROSSUM_MCP_MODE", "read-write").lower()
9
+
10
+
11
+ def build_resource_url(resource_type: str, resource_id: int) -> str:
12
+ """Build a full URL for a Rossum API resource."""
13
+ return f"{BASE_URL}/{resource_type}/{resource_id}"
14
+
15
+
16
+ def is_read_write_mode() -> bool:
17
+ """Check if server is in read-write mode."""
18
+ return MODE == "read-write"
@@ -0,0 +1,58 @@
1
+ """Document relation tools for Rossum MCP Server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING
7
+
8
+ from rossum_api.models.document_relation import (
9
+ DocumentRelation, # noqa: TC002 - needed at runtime for FastMCP
10
+ )
11
+
12
+ if TYPE_CHECKING:
13
+ from fastmcp import FastMCP
14
+ from rossum_api import AsyncRossumAPIClient
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def register_document_relation_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None:
20
+ """Register document relation-related tools with the FastMCP server."""
21
+
22
+ @mcp.tool(description="Retrieve document relation details.")
23
+ async def get_document_relation(document_relation_id: int) -> DocumentRelation:
24
+ """Retrieve document relation details."""
25
+ logger.debug(f"Retrieving document relation: document_relation_id={document_relation_id}")
26
+ document_relation: DocumentRelation = await client.retrieve_document_relation(document_relation_id)
27
+ return document_relation
28
+
29
+ @mcp.tool(
30
+ description="List all document relations with optional filters. Document relations introduce additional relations between annotations and documents (export, einvoice)."
31
+ )
32
+ async def list_document_relations(
33
+ id: int | None = None,
34
+ type: str | None = None,
35
+ annotation: int | None = None,
36
+ key: str | None = None,
37
+ documents: int | None = None,
38
+ ) -> list[DocumentRelation]:
39
+ """List all document relations with optional filters."""
40
+ logger.debug(
41
+ f"Listing document relations: id={id}, type={type}, annotation={annotation}, key={key}, documents={documents}"
42
+ )
43
+ filters: dict[str, int | str] = {}
44
+ if id is not None:
45
+ filters["id"] = id
46
+ if type is not None:
47
+ filters["type"] = type
48
+ if annotation is not None:
49
+ filters["annotation"] = annotation
50
+ if key is not None:
51
+ filters["key"] = key
52
+ if documents is not None:
53
+ filters["documents"] = documents
54
+
55
+ return [
56
+ document_relation
57
+ async for document_relation in client.list_document_relations(**filters) # type: ignore[arg-type]
58
+ ]
@@ -0,0 +1,130 @@
1
+ """Engine tools for Rossum MCP Server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING, Literal
7
+
8
+ from rossum_api.domain_logic.resources import Resource
9
+ from rossum_api.models.engine import (
10
+ Engine, # noqa: TC002 - needed at runtime for FastMCP
11
+ EngineField, # noqa: TC002 - needed at runtime for FastMCP
12
+ EngineFieldType, # noqa: TC002 - needed at runtime for FastMCP
13
+ )
14
+
15
+ from rossum_mcp.tools.base import build_resource_url, is_read_write_mode
16
+
17
+ type EngineType = Literal["extractor", "splitter"]
18
+
19
+ if TYPE_CHECKING:
20
+ from fastmcp import FastMCP
21
+ from rossum_api import AsyncRossumAPIClient
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ def register_engine_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None: # noqa: C901
27
+ """Register engine-related tools with the FastMCP server."""
28
+
29
+ @mcp.tool(description="Retrieve a single engine by ID.")
30
+ async def get_engine(engine_id: int) -> Engine:
31
+ """Retrieve a single engine by ID."""
32
+ logger.debug(f"Retrieving engine: engine_id={engine_id}")
33
+ engine: Engine = await client.retrieve_engine(engine_id)
34
+ return engine
35
+
36
+ @mcp.tool(description="List all engines with optional filters.")
37
+ async def list_engines(
38
+ id: int | None = None, engine_type: EngineType | None = None, agenda_id: str | None = None
39
+ ) -> list[Engine]:
40
+ """List all engines with optional filters."""
41
+ logger.debug(f"Listing engines: id={id}, type={engine_type}, agenda_id={agenda_id}")
42
+ filters: dict[str, int | str] = {}
43
+ if id is not None:
44
+ filters["id"] = id
45
+ if engine_type is not None:
46
+ filters["type"] = engine_type
47
+ if agenda_id is not None:
48
+ filters["agenda_id"] = agenda_id
49
+ return [engine async for engine in client.list_engines(**filters)] # type: ignore[arg-type]
50
+
51
+ @mcp.tool(description="Update engine settings.")
52
+ async def update_engine(engine_id: int, engine_data: dict) -> Engine | dict:
53
+ """Update an existing engine's settings."""
54
+ if not is_read_write_mode():
55
+ return {"error": "update_engine is not available in read-only mode"}
56
+
57
+ logger.debug(f"Updating engine: engine_id={engine_id}, data={engine_data}")
58
+ updated_engine_data = await client._http_client.update(Resource.Engine, engine_id, engine_data)
59
+ updated_engine: Engine = client._deserializer(Resource.Engine, updated_engine_data)
60
+ return updated_engine
61
+
62
+ @mcp.tool(
63
+ description="Create a new engine. IMPORTANT: When creating a new engine, check the schema to be used and create contained Engine fields immediately!"
64
+ )
65
+ async def create_engine(name: str, organization_id: int, engine_type: EngineType) -> Engine | dict:
66
+ """Create a new engine."""
67
+ if not is_read_write_mode():
68
+ return {"error": "create_engine is not available in read-only mode"}
69
+
70
+ if engine_type not in ("extractor", "splitter"):
71
+ raise ValueError(f"Invalid engine_type '{engine_type}'. Must be 'extractor' or 'splitter'")
72
+
73
+ logger.debug(f"Creating engine: name={name}, organization_id={organization_id}, type={engine_type}")
74
+ engine_data = {
75
+ "name": name,
76
+ "organization": build_resource_url("organizations", organization_id),
77
+ "type": engine_type,
78
+ }
79
+ engine_response = await client._http_client.create(Resource.Engine, engine_data)
80
+ engine: Engine = client._deserializer(Resource.Engine, engine_response)
81
+ return engine
82
+
83
+ @mcp.tool(description="Create engine field for each schema field. Must be called when creating engine + schema.")
84
+ async def create_engine_field(
85
+ engine_id: int,
86
+ name: str,
87
+ label: str,
88
+ field_type: EngineFieldType,
89
+ schema_ids: list[int],
90
+ tabular: bool = False,
91
+ multiline: str = "false",
92
+ subtype: str | None = None,
93
+ pre_trained_field_id: str | None = None,
94
+ ) -> EngineField | dict:
95
+ """Create a new engine field and link it to schemas."""
96
+ if not is_read_write_mode():
97
+ return {"error": "create_engine_field is not available in read-only mode"}
98
+
99
+ valid_types = ("string", "number", "date", "enum")
100
+ if field_type not in valid_types:
101
+ raise ValueError(f"Invalid field_type '{field_type}'. Must be one of: {', '.join(valid_types)}")
102
+ if not schema_ids:
103
+ raise ValueError("schema_ids cannot be empty - engine field must be linked to at least one schema")
104
+
105
+ logger.debug(
106
+ f"Creating engine field: engine_id={engine_id}, name={name}, type={field_type}, schemas={schema_ids}"
107
+ )
108
+ engine_field_data = {
109
+ "engine": build_resource_url("engines", engine_id),
110
+ "name": name,
111
+ "label": label,
112
+ "type": field_type,
113
+ "tabular": tabular,
114
+ "multiline": multiline,
115
+ "schemas": [build_resource_url("schemas", schema_id) for schema_id in schema_ids],
116
+ }
117
+ if subtype is not None:
118
+ engine_field_data["subtype"] = subtype
119
+ if pre_trained_field_id is not None:
120
+ engine_field_data["pre_trained_field_id"] = pre_trained_field_id
121
+
122
+ engine_field_response = await client._http_client.create(Resource.EngineField, engine_field_data)
123
+ engine_field: EngineField = client._deserializer(Resource.EngineField, engine_field_response)
124
+ return engine_field
125
+
126
+ @mcp.tool(description="Retrieve engine fields for a specific engine or all engine fields.")
127
+ async def get_engine_fields(engine_id: int | None = None) -> list[EngineField]:
128
+ """Retrieve engine fields for a specific engine or all engine fields."""
129
+ logger.debug(f"Retrieving engine fields: engine_id={engine_id}")
130
+ return [engine_field async for engine_field in client.retrieve_engine_fields(engine_id=engine_id)]