rossum-mcp 0.3.4__py3-none-any.whl → 0.3.5__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 CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.3.4"
3
+ __version__ = "0.3.5"
rossum_mcp/server.py CHANGED
@@ -18,6 +18,7 @@ from rossum_mcp.logging_config import setup_logging
18
18
  from rossum_mcp.tools import (
19
19
  register_annotation_tools,
20
20
  register_document_relation_tools,
21
+ register_email_template_tools,
21
22
  register_engine_tools,
22
23
  register_hook_tools,
23
24
  register_queue_tools,
@@ -49,6 +50,7 @@ register_queue_tools(mcp, client)
49
50
  register_schema_tools(mcp, client)
50
51
  register_engine_tools(mcp, client)
51
52
  register_hook_tools(mcp, client)
53
+ register_email_template_tools(mcp, client)
52
54
  register_document_relation_tools(mcp, client)
53
55
  register_relation_tools(mcp, client)
54
56
  register_rule_tools(mcp, client)
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from rossum_mcp.tools.annotations import register_annotation_tools
6
6
  from rossum_mcp.tools.document_relations import register_document_relation_tools
7
+ from rossum_mcp.tools.email_templates import register_email_template_tools
7
8
  from rossum_mcp.tools.engines import register_engine_tools
8
9
  from rossum_mcp.tools.hooks import register_hook_tools
9
10
  from rossum_mcp.tools.queues import register_queue_tools
@@ -16,6 +17,7 @@ from rossum_mcp.tools.workspaces import register_workspace_tools
16
17
  __all__ = [
17
18
  "register_annotation_tools",
18
19
  "register_document_relation_tools",
20
+ "register_email_template_tools",
19
21
  "register_engine_tools",
20
22
  "register_hook_tools",
21
23
  "register_queue_tools",
@@ -7,7 +7,7 @@ from collections.abc import Sequence # noqa: TC003 - needed at runtime for Fast
7
7
  from pathlib import Path
8
8
  from typing import TYPE_CHECKING, Literal
9
9
 
10
- from rossum_api.models.annotation import Annotation # noqa: TC002 - needed at runtime for FastMCP
10
+ from rossum_api.models.annotation import Annotation
11
11
 
12
12
  from rossum_mcp.tools.base import is_read_write_mode
13
13
 
@@ -17,114 +17,151 @@ if TYPE_CHECKING:
17
17
 
18
18
  logger = logging.getLogger(__name__)
19
19
 
20
- # Fixed sideloads (critical for well-behaving agent)
21
20
  type Sideload = Literal["content", "document", "automation_blocker"]
22
21
 
23
22
 
24
- def register_annotation_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None: # noqa: C901
23
+ async def _upload_document(client: AsyncRossumAPIClient, file_path: str, queue_id: int) -> dict:
24
+ if not is_read_write_mode():
25
+ return {"error": "upload_document is not available in read-only mode"}
26
+
27
+ path = Path(file_path)
28
+ if not path.exists():
29
+ logger.error(f"File not found: {file_path}")
30
+ raise FileNotFoundError(f"File not found: {file_path}")
31
+
32
+ try:
33
+ task = (await client.upload_document(queue_id, [(str(path), path.name)]))[0]
34
+ except KeyError as e:
35
+ logger.error(f"Upload failed - unexpected API response format: {e!s}")
36
+ error_msg = (
37
+ f"Document upload failed - API response missing expected key {e!s}. "
38
+ f"This usually means either:\n"
39
+ f"1. The queue_id ({queue_id}) is invalid or you don't have access to it\n"
40
+ f"2. The Rossum API returned an error response\n"
41
+ f"Please verify:\n"
42
+ f"- The queue_id is correct and exists in your workspace\n"
43
+ f"- You have permission to upload documents to this queue\n"
44
+ f"- Your API token has the necessary permissions"
45
+ )
46
+ raise ValueError(error_msg) from e
47
+ except IndexError as e:
48
+ logger.error(f"Upload failed - no tasks returned: {e}")
49
+ raise ValueError(
50
+ f"Document upload failed - no tasks were created. This may indicate the queue_id ({queue_id}) is invalid."
51
+ ) from e
52
+ except Exception as e:
53
+ logger.error(f"Upload failed: {type(e).__name__}: {e}")
54
+ raise ValueError(f"Document upload failed: {type(e).__name__}: {e!s}") from e
55
+
56
+ return {
57
+ "task_id": task.id,
58
+ "task_status": task.status,
59
+ "queue_id": queue_id,
60
+ "message": "Document upload initiated. Use `list_annotations` to find the annotation ID for this queue.",
61
+ }
62
+
63
+
64
+ async def _get_annotation(
65
+ client: AsyncRossumAPIClient, annotation_id: int, sideloads: Sequence[Sideload] = ()
66
+ ) -> Annotation:
67
+ logger.debug(f"Retrieving annotation: annotation_id={annotation_id}")
68
+ annotation: Annotation = await client.retrieve_annotation(annotation_id, sideloads) # type: ignore[arg-type]
69
+ return annotation
70
+
71
+
72
+ async def _list_annotations(
73
+ client: AsyncRossumAPIClient,
74
+ queue_id: int,
75
+ status: str | None = "importing,to_review,confirmed,exported",
76
+ ordering: Sequence[str] = (),
77
+ first_n: int | None = None,
78
+ ) -> list[Annotation]:
79
+ logger.debug(f"Listing annotations: queue_id={queue_id}, status={status}, ordering={ordering}, first_n={first_n}")
80
+ params: dict = {"queue": queue_id, "page_size": 100}
81
+ if status:
82
+ params["status"] = status
83
+ if ordering:
84
+ params["ordering"] = ordering
85
+
86
+ annotations_list: list[Annotation] = []
87
+ async for item in client.list_annotations(**params):
88
+ annotations_list.append(item)
89
+ if first_n is not None and len(annotations_list) >= first_n:
90
+ break
91
+ return annotations_list
92
+
93
+
94
+ async def _start_annotation(client: AsyncRossumAPIClient, annotation_id: int) -> dict:
95
+ if not is_read_write_mode():
96
+ return {"error": "start_annotation is not available in read-only mode"}
97
+
98
+ logger.debug(f"Starting annotation: annotation_id={annotation_id}")
99
+ await client.start_annotation(annotation_id)
100
+ return {
101
+ "annotation_id": annotation_id,
102
+ "message": f"Annotation {annotation_id} started successfully. Status changed to 'reviewing'.",
103
+ }
104
+
105
+
106
+ async def _bulk_update_annotation_fields(
107
+ client: AsyncRossumAPIClient, annotation_id: int, operations: list[dict]
108
+ ) -> dict:
109
+ if not is_read_write_mode():
110
+ return {"error": "bulk_update_annotation_fields is not available in read-only mode"}
111
+
112
+ logger.debug(f"Bulk updating annotation: annotation_id={annotation_id}, ops={operations}")
113
+ await client.bulk_update_annotation_data(annotation_id, operations)
114
+ return {
115
+ "annotation_id": annotation_id,
116
+ "operations_count": len(operations),
117
+ "message": f"Annotation {annotation_id} updated with {len(operations)} operations successfully.",
118
+ }
119
+
120
+
121
+ async def _confirm_annotation(client: AsyncRossumAPIClient, annotation_id: int) -> dict:
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
+ }
131
+
132
+
133
+ def register_annotation_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None:
25
134
  """Register annotation-related tools with the FastMCP server."""
26
135
 
27
136
  @mcp.tool(description="Upload a document to Rossum. Use list_annotations to get annotation ID.")
28
137
  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
- }
138
+ return await _upload_document(client, file_path, queue_id)
68
139
 
69
140
  @mcp.tool(description="Retrieve annotation data. Use 'content' sideload to get extracted data.")
70
141
  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
142
+ return await _get_annotation(client, annotation_id, sideloads)
75
143
 
76
- @mcp.tool(description="List annotations for a queue.")
144
+ @mcp.tool(description="List annotations for a queue. Use ordering=['-created_at'] to sort by newest first.")
77
145
  async def list_annotations(
78
- queue_id: int, status: str | None = "importing,to_review,confirmed,exported"
146
+ queue_id: int,
147
+ status: str | None = "importing,to_review,confirmed,exported",
148
+ ordering: Sequence[str] = (),
149
+ first_n: int | None = None,
79
150
  ) -> 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
151
+ return await _list_annotations(client, queue_id, status, ordering, first_n)
87
152
 
88
153
  @mcp.tool(description="Start annotation (move from 'to_review' to 'reviewing').")
89
154
  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
- }
155
+ return await _start_annotation(client, annotation_id)
100
156
 
101
157
  @mcp.tool(
102
158
  description="Bulk update annotation fields. It can be used after `start_annotation` only. Use datapoint ID from content, NOT schema_id."
103
159
  )
104
160
  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
- }
161
+ return await _bulk_update_annotation_fields(client, annotation_id, operations)
116
162
 
117
163
  @mcp.tool(
118
164
  description="Confirm annotation (move to 'confirmed'). It can be used after `bulk_update_annotation_fields`."
119
165
  )
120
166
  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
- }
167
+ return await _confirm_annotation(client, annotation_id)
rossum_mcp/tools/base.py CHANGED
@@ -3,10 +3,17 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from typing import Any
6
10
 
7
11
  BASE_URL = os.environ.get("ROSSUM_API_BASE_URL", "").rstrip("/")
8
12
  MODE = os.environ.get("ROSSUM_MCP_MODE", "read-write").lower()
9
13
 
14
+ # Marker used to indicate omitted fields in list responses
15
+ TRUNCATED_MARKER = "<omitted>"
16
+
10
17
 
11
18
  def build_resource_url(resource_type: str, resource_id: int) -> str:
12
19
  """Build a full URL for a Rossum API resource."""
@@ -16,3 +23,18 @@ def build_resource_url(resource_type: str, resource_id: int) -> str:
16
23
  def is_read_write_mode() -> bool:
17
24
  """Check if server is in read-write mode."""
18
25
  return MODE == "read-write"
26
+
27
+
28
+ def truncate_dict_fields(data: dict[str, Any], fields: tuple[str, ...]) -> dict[str, Any]:
29
+ """Truncate specified fields in a dictionary to save context.
30
+
31
+ Returns a new dictionary with specified fields replaced by TRUNCATED_MARKER.
32
+ """
33
+ if not data:
34
+ return data
35
+
36
+ result = dict(data)
37
+ for field in fields:
38
+ if field in result:
39
+ result[field] = TRUNCATED_MARKER
40
+ return result
@@ -5,9 +5,7 @@ from __future__ import annotations
5
5
  import logging
6
6
  from typing import TYPE_CHECKING
7
7
 
8
- from rossum_api.models.document_relation import (
9
- DocumentRelation, # noqa: TC002 - needed at runtime for FastMCP
10
- )
8
+ from rossum_api.models.document_relation import DocumentRelation
11
9
 
12
10
  if TYPE_CHECKING:
13
11
  from fastmcp import FastMCP
@@ -0,0 +1,131 @@
1
+ """Email template tools for Rossum MCP Server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING, Any, Literal
7
+
8
+ from rossum_api.models.email_template import EmailTemplate
9
+
10
+ from rossum_mcp.tools.base import is_read_write_mode
11
+
12
+ if TYPE_CHECKING:
13
+ from fastmcp import FastMCP
14
+ from rossum_api import AsyncRossumAPIClient
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ EmailTemplateType = Literal["rejection", "rejection_default", "email_with_no_processable_attachments", "custom"]
19
+
20
+
21
+ async def _get_email_template(client: AsyncRossumAPIClient, email_template_id: int) -> EmailTemplate:
22
+ email_template: EmailTemplate = await client.retrieve_email_template(email_template_id)
23
+ return email_template
24
+
25
+
26
+ async def _list_email_templates(
27
+ client: AsyncRossumAPIClient,
28
+ queue_id: int | None = None,
29
+ type: EmailTemplateType | None = None,
30
+ name: str | None = None,
31
+ first_n: int | None = None,
32
+ ) -> list[EmailTemplate]:
33
+ filters: dict = {}
34
+ if queue_id is not None:
35
+ filters["queue"] = queue_id
36
+ if type is not None:
37
+ filters["type"] = type
38
+ if name is not None:
39
+ filters["name"] = name
40
+
41
+ if first_n is not None:
42
+ templates_iter = client.list_email_templates(**filters)
43
+ templates_list: list[EmailTemplate] = []
44
+ n = 0
45
+ while n < first_n:
46
+ templates_list.append(await anext(templates_iter))
47
+ n += 1
48
+ else:
49
+ templates_list = [template async for template in client.list_email_templates(**filters)]
50
+
51
+ return templates_list
52
+
53
+
54
+ async def _create_email_template(
55
+ client: AsyncRossumAPIClient,
56
+ name: str,
57
+ queue: str,
58
+ subject: str,
59
+ message: str,
60
+ type: EmailTemplateType = "custom",
61
+ automate: bool = False,
62
+ to: list[dict[str, Any]] | None = None,
63
+ cc: list[dict[str, Any]] | None = None,
64
+ bcc: list[dict[str, Any]] | None = None,
65
+ triggers: list[str] | None = None,
66
+ ) -> EmailTemplate | dict:
67
+ if not is_read_write_mode():
68
+ return {"error": "create_email_template is not available in read-only mode"}
69
+
70
+ logger.debug(f"Creating email template: name={name}, queue={queue}, type={type}")
71
+
72
+ template_data: dict[str, Any] = {
73
+ "name": name,
74
+ "queue": queue,
75
+ "subject": subject,
76
+ "message": message,
77
+ "type": type,
78
+ "automate": automate,
79
+ }
80
+
81
+ if to is not None:
82
+ template_data["to"] = to
83
+ if cc is not None:
84
+ template_data["cc"] = cc
85
+ if bcc is not None:
86
+ template_data["bcc"] = bcc
87
+ if triggers is not None:
88
+ template_data["triggers"] = triggers
89
+
90
+ email_template: EmailTemplate = await client.create_new_email_template(template_data)
91
+ return email_template
92
+
93
+
94
+ def register_email_template_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None:
95
+ """Register email template-related tools with the FastMCP server."""
96
+
97
+ @mcp.tool(
98
+ description="Retrieve a single email template by ID. Use list_email_templates first to find templates for a queue."
99
+ )
100
+ async def get_email_template(email_template_id: int) -> EmailTemplate:
101
+ return await _get_email_template(client, email_template_id)
102
+
103
+ @mcp.tool(
104
+ description="List all email templates with optional filtering. Email templates define automated or manual email responses sent from Rossum queues. Types: 'rejection' (for rejecting documents), 'rejection_default' (default rejection template), 'email_with_no_processable_attachments' (when email has no valid attachments), 'custom' (user-defined templates)."
105
+ )
106
+ async def list_email_templates(
107
+ queue_id: int | None = None,
108
+ type: EmailTemplateType | None = None,
109
+ name: str | None = None,
110
+ first_n: int | None = None,
111
+ ) -> list[EmailTemplate]:
112
+ return await _list_email_templates(client, queue_id, type, name, first_n)
113
+
114
+ @mcp.tool(
115
+ description="Create a new email template. Email templates can be automated (automate=True) to send emails automatically on specific triggers, or manual for user-initiated sending. The 'to', 'cc', and 'bcc' fields accept lists of recipient objects with 'type' ('annotator', 'constant', 'datapoint') and 'value' keys."
116
+ )
117
+ async def create_email_template(
118
+ name: str,
119
+ queue: str,
120
+ subject: str,
121
+ message: str,
122
+ type: EmailTemplateType = "custom",
123
+ automate: bool = False,
124
+ to: list[dict[str, Any]] | None = None,
125
+ cc: list[dict[str, Any]] | None = None,
126
+ bcc: list[dict[str, Any]] | None = None,
127
+ triggers: list[str] | None = None,
128
+ ) -> EmailTemplate | dict:
129
+ return await _create_email_template(
130
+ client, name, queue, subject, message, type, automate, to, cc, bcc, triggers
131
+ )