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 +3 -0
- rossum_mcp/logging_config.py +141 -0
- rossum_mcp/py.typed +0 -0
- rossum_mcp/server.py +65 -0
- rossum_mcp/tools/__init__.py +27 -0
- rossum_mcp/tools/annotations.py +130 -0
- rossum_mcp/tools/base.py +18 -0
- rossum_mcp/tools/document_relations.py +58 -0
- rossum_mcp/tools/engines.py +130 -0
- rossum_mcp/tools/hooks.py +265 -0
- rossum_mcp/tools/queues.py +133 -0
- rossum_mcp/tools/relations.py +56 -0
- rossum_mcp/tools/rules.py +42 -0
- rossum_mcp/tools/schemas.py +384 -0
- rossum_mcp/tools/users.py +60 -0
- rossum_mcp/tools/workspaces.py +65 -0
- rossum_mcp-0.3.4.dist-info/METADATA +1537 -0
- rossum_mcp-0.3.4.dist-info/RECORD +21 -0
- rossum_mcp-0.3.4.dist-info/WHEEL +5 -0
- rossum_mcp-0.3.4.dist-info/entry_points.txt +2 -0
- rossum_mcp-0.3.4.dist-info/top_level.txt +1 -0
rossum_mcp/__init__.py
ADDED
|
@@ -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
|
+
}
|
rossum_mcp/tools/base.py
ADDED
|
@@ -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)]
|