rossum-mcp 0.3.4__py3-none-any.whl → 0.4.0__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.
@@ -4,16 +4,17 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  import os
7
- from typing import TYPE_CHECKING
7
+ from dataclasses import replace
8
+ from typing import TYPE_CHECKING, Literal, cast, get_args
8
9
 
9
10
  from rossum_api import APIClientError
10
11
  from rossum_api.domain_logic.resources import Resource
11
12
  from rossum_api.models import deserialize_default
12
- from rossum_api.models.engine import Engine # noqa: TC002 - needed at runtime for FastMCP
13
- from rossum_api.models.queue import Queue # noqa: TC002 - needed at runtime for FastMCP
14
- from rossum_api.models.schema import Schema # noqa: TC002 - needed at runtime for FastMCP
13
+ from rossum_api.models.engine import Engine
14
+ from rossum_api.models.queue import Queue
15
+ from rossum_api.models.schema import Schema
15
16
 
16
- from rossum_mcp.tools.base import build_resource_url, is_read_write_mode
17
+ from rossum_mcp.tools.base import build_resource_url, is_read_write_mode, truncate_dict_fields
17
18
 
18
19
  if TYPE_CHECKING:
19
20
  from fastmcp import FastMCP
@@ -21,56 +22,222 @@ if TYPE_CHECKING:
21
22
 
22
23
  logger = logging.getLogger(__name__)
23
24
 
25
+ # Fields to truncate in queue.settings for list responses
26
+ _QUEUE_SETTINGS_TRUNCATE_FIELDS = (
27
+ "accepted_mime_types",
28
+ "annotation_list_table",
29
+ "users",
30
+ "dashboard_customization",
31
+ "email_notifications",
32
+ )
24
33
 
25
- def register_queue_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None: # noqa: C901
34
+
35
+ def _truncate_queue_for_list(queue: Queue) -> Queue:
36
+ """Truncate verbose fields in queue settings to save context in list responses."""
37
+ if not queue.settings:
38
+ return queue
39
+ return replace(queue, settings=truncate_dict_fields(queue.settings, _QUEUE_SETTINGS_TRUNCATE_FIELDS))
40
+
41
+
42
+ async def _get_queue(client: AsyncRossumAPIClient, queue_id: int) -> Queue:
43
+ logger.debug(f"Retrieving queue: queue_id={queue_id}")
44
+ queue: Queue = await client.retrieve_queue(queue_id)
45
+ return queue
46
+
47
+
48
+ async def _list_queues(
49
+ client: AsyncRossumAPIClient, workspace_id: int | None = None, name: str | None = None
50
+ ) -> list[Queue]:
51
+ logger.debug(f"Listing queues: workspace_id={workspace_id}, name={name}")
52
+ filters: dict[str, int | str] = {}
53
+ if workspace_id is not None:
54
+ filters["workspace"] = workspace_id
55
+ if name is not None:
56
+ filters["name"] = name
57
+
58
+ queues = [queue async for queue in client.list_queues(**filters)] # type: ignore[arg-type]
59
+ return [_truncate_queue_for_list(queue) for queue in queues]
60
+
61
+
62
+ async def _get_queue_schema(client: AsyncRossumAPIClient, queue_id: int) -> Schema:
63
+ logger.debug(f"Retrieving queue schema: queue_id={queue_id}")
64
+ queue: Queue = await client.retrieve_queue(queue_id)
65
+ schema_url = queue.schema
66
+ schema_id = int(schema_url.rstrip("/").split("/")[-1])
67
+ schema: Schema = await client.retrieve_schema(schema_id)
68
+ return schema
69
+
70
+
71
+ async def _get_queue_engine(client: AsyncRossumAPIClient, queue_id: int) -> Engine | dict:
72
+ logger.debug(f"Retrieving queue engine: queue_id={queue_id}")
73
+ queue: Queue = await client.retrieve_queue(queue_id)
74
+
75
+ engine_url = None
76
+ if queue.dedicated_engine:
77
+ engine_url = queue.dedicated_engine
78
+ elif queue.generic_engine:
79
+ engine_url = queue.generic_engine
80
+ elif queue.engine:
81
+ engine_url = queue.engine
82
+
83
+ if not engine_url:
84
+ return {"message": "No engine assigned to this queue"}
85
+
86
+ try:
87
+ if isinstance(engine_url, str):
88
+ engine_id = int(engine_url.rstrip("/").split("/")[-1])
89
+ engine: Engine = await client.retrieve_engine(engine_id)
90
+ else:
91
+ engine = deserialize_default(Resource.Engine, engine_url)
92
+ except APIClientError as e:
93
+ if e.status_code == 404:
94
+ return {"message": f"Engine not found (engine URL: {engine_url})"}
95
+ raise
96
+
97
+ return engine
98
+
99
+
100
+ async def _create_queue(
101
+ client: AsyncRossumAPIClient,
102
+ name: str,
103
+ workspace_id: int,
104
+ schema_id: int,
105
+ engine_id: int | None = None,
106
+ inbox_id: int | None = None,
107
+ connector_id: int | None = None,
108
+ locale: str = "en_GB",
109
+ automation_enabled: bool = False,
110
+ automation_level: str = "never",
111
+ training_enabled: bool = True,
112
+ splitting_screen_feature_flag: bool = False,
113
+ ) -> Queue | dict:
114
+ if not is_read_write_mode():
115
+ return {"error": "create_queue is not available in read-only mode"}
116
+
117
+ logger.debug(
118
+ f"Creating queue: name={name}, workspace_id={workspace_id}, schema_id={schema_id}, engine_id={engine_id}"
119
+ )
120
+
121
+ queue_data: dict = {
122
+ "name": name,
123
+ "workspace": build_resource_url("workspaces", workspace_id),
124
+ "schema": build_resource_url("schemas", schema_id),
125
+ "locale": locale,
126
+ "automation_enabled": automation_enabled,
127
+ "automation_level": automation_level,
128
+ "training_enabled": training_enabled,
129
+ }
130
+
131
+ if engine_id is not None:
132
+ queue_data["engine"] = build_resource_url("engines", engine_id)
133
+ if inbox_id is not None:
134
+ queue_data["inbox"] = build_resource_url("inboxes", inbox_id)
135
+ if connector_id is not None:
136
+ queue_data["connector"] = build_resource_url("connectors", connector_id)
137
+ if splitting_screen_feature_flag:
138
+ if os.environ.get("SPLITTING_SCREEN_FLAG_NAME") and os.environ.get("SPLITTING_SCREEN_FLAG_VALUE"):
139
+ queue_data["settings"] = {
140
+ os.environ["SPLITTING_SCREEN_FLAG_NAME"]: os.environ["SPLITTING_SCREEN_FLAG_VALUE"]
141
+ }
142
+ else:
143
+ logger.error("Splitting screen failed to update")
144
+
145
+ queue: Queue = await client.create_new_queue(queue_data)
146
+ return queue
147
+
148
+
149
+ async def _update_queue(client: AsyncRossumAPIClient, queue_id: int, queue_data: dict) -> Queue | dict:
150
+ if not is_read_write_mode():
151
+ return {"error": "update_queue is not available in read-only mode"}
152
+
153
+ logger.debug(f"Updating queue: queue_id={queue_id}, data={queue_data}")
154
+ updated_queue_data = await client._http_client.update(Resource.Queue, queue_id, queue_data)
155
+ return cast("Queue", client._deserializer(Resource.Queue, updated_queue_data))
156
+
157
+
158
+ # Available template names for create_queue_from_template
159
+ QueueTemplateName = Literal[
160
+ "EU Demo Template",
161
+ "AP&R EU Demo Template",
162
+ "Tax Invoice EU Demo Template",
163
+ "US Demo Template",
164
+ "AP&R US Demo Template",
165
+ "Tax Invoice US Demo Template",
166
+ "UK Demo Template",
167
+ "AP&R UK Demo Template",
168
+ "Tax Invoice UK Demo Template",
169
+ "CZ Demo Template",
170
+ "Empty Organization Template",
171
+ "Delivery Notes Demo Template",
172
+ "Delivery Note Demo Template",
173
+ "Chinese Invoices (Fapiao) Demo Template",
174
+ "Tax Invoice CN Demo Template",
175
+ "Certificates of Analysis Demo Template",
176
+ "Purchase Order Demo Template",
177
+ "Credit Note Demo Template",
178
+ "Debit Note Demo Template",
179
+ "Proforma Invoice Demo Template",
180
+ ]
181
+ QUEUE_TEMPLATE_NAMES = get_args(QueueTemplateName)
182
+
183
+
184
+ async def _create_queue_from_template(
185
+ client: AsyncRossumAPIClient,
186
+ name: str,
187
+ template_name: QueueTemplateName,
188
+ workspace_id: int,
189
+ include_documents: bool = False,
190
+ engine_id: int | None = None,
191
+ ) -> Queue | dict:
192
+ if not is_read_write_mode():
193
+ return {"error": "create_queue_from_template is not available in read-only mode"}
194
+
195
+ if template_name not in QUEUE_TEMPLATE_NAMES:
196
+ return {
197
+ "error": f"Invalid template_name: '{template_name}'",
198
+ "available_templates": QUEUE_TEMPLATE_NAMES,
199
+ }
200
+
201
+ logger.debug(
202
+ f"Creating queue from template: name={name}, template_name={template_name}, workspace_id={workspace_id}"
203
+ )
204
+
205
+ payload: dict = {
206
+ "name": name,
207
+ "template_name": template_name,
208
+ "workspace": build_resource_url("workspaces", workspace_id),
209
+ "include_documents": include_documents,
210
+ }
211
+
212
+ if engine_id is not None:
213
+ payload["engine"] = build_resource_url("engines", engine_id)
214
+
215
+ response = await client._http_client.request_json(
216
+ method="POST",
217
+ url="queues/from_template",
218
+ json=payload,
219
+ )
220
+ return cast("Queue", client._deserializer(Resource.Queue, response))
221
+
222
+
223
+ def register_queue_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None:
26
224
  """Register queue-related tools with the FastMCP server."""
27
225
 
28
226
  @mcp.tool(description="Retrieve queue details.")
29
227
  async def get_queue(queue_id: int) -> Queue:
30
- """Retrieve queue details."""
31
- logger.debug(f"Retrieving queue: queue_id={queue_id}")
32
- queue: Queue = await client.retrieve_queue(queue_id)
33
- return queue
228
+ return await _get_queue(client, queue_id)
229
+
230
+ @mcp.tool(description="List all queues with optional filters.")
231
+ async def list_queues(workspace_id: int | None = None, name: str | None = None) -> list[Queue]:
232
+ return await _list_queues(client, workspace_id, name)
34
233
 
35
234
  @mcp.tool(description="Retrieve queue schema.")
36
235
  async def get_queue_schema(queue_id: int) -> Schema:
37
- """Retrieve complete schema for a queue."""
38
- logger.debug(f"Retrieving queue schema: queue_id={queue_id}")
39
- queue: Queue = await client.retrieve_queue(queue_id)
40
- schema_url = queue.schema
41
- schema_id = int(schema_url.rstrip("/").split("/")[-1])
42
- schema: Schema = await client.retrieve_schema(schema_id)
43
- return schema
236
+ return await _get_queue_schema(client, queue_id)
44
237
 
45
238
  @mcp.tool(description="Retrieve queue engine. Returns None if no engine assigned.")
46
239
  async def get_queue_engine(queue_id: int) -> Engine | dict:
47
- """Retrieve complete engine information for a queue."""
48
- logger.debug(f"Retrieving queue engine: queue_id={queue_id}")
49
- queue: Queue = await client.retrieve_queue(queue_id)
50
-
51
- engine_url = None
52
- if queue.dedicated_engine:
53
- engine_url = queue.dedicated_engine
54
- elif queue.generic_engine:
55
- engine_url = queue.generic_engine
56
- elif queue.engine:
57
- engine_url = queue.engine
58
-
59
- if not engine_url:
60
- return {"message": "No engine assigned to this queue"}
61
-
62
- try:
63
- if isinstance(engine_url, str):
64
- engine_id = int(engine_url.rstrip("/").split("/")[-1])
65
- engine: Engine = await client.retrieve_engine(engine_id)
66
- else:
67
- engine = deserialize_default(Resource.Engine, engine_url)
68
- except APIClientError as e:
69
- if e.status_code == 404:
70
- return {"message": f"Engine not found (engine URL: {engine_url})"}
71
- raise
72
-
73
- return engine
240
+ return await _get_queue_engine(client, queue_id)
74
241
 
75
242
  @mcp.tool(description="Create a queue.")
76
243
  async def create_queue(
@@ -86,48 +253,40 @@ def register_queue_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None: #
86
253
  training_enabled: bool = True,
87
254
  splitting_screen_feature_flag: bool = False,
88
255
  ) -> Queue | dict:
89
- """Create a new queue with schema and optional engine assignment."""
90
- if not is_read_write_mode():
91
- return {"error": "create_queue is not available in read-only mode"}
92
-
93
- logger.debug(
94
- f"Creating queue: name={name}, workspace_id={workspace_id}, schema_id={schema_id}, engine_id={engine_id}"
256
+ return await _create_queue(
257
+ client,
258
+ name,
259
+ workspace_id,
260
+ schema_id,
261
+ engine_id,
262
+ inbox_id,
263
+ connector_id,
264
+ locale,
265
+ automation_enabled,
266
+ automation_level,
267
+ training_enabled,
268
+ splitting_screen_feature_flag,
95
269
  )
96
270
 
97
- queue_data: dict = {
98
- "name": name,
99
- "workspace": build_resource_url("workspaces", workspace_id),
100
- "schema": build_resource_url("schemas", schema_id),
101
- "locale": locale,
102
- "automation_enabled": automation_enabled,
103
- "automation_level": automation_level,
104
- "training_enabled": training_enabled,
105
- }
106
-
107
- if engine_id is not None:
108
- queue_data["engine"] = build_resource_url("engines", engine_id)
109
- if inbox_id is not None:
110
- queue_data["inbox"] = build_resource_url("inboxes", inbox_id)
111
- if connector_id is not None:
112
- queue_data["connector"] = build_resource_url("connectors", connector_id)
113
- if splitting_screen_feature_flag:
114
- if os.environ.get("SPLITTING_SCREEN_FLAG_NAME") and os.environ.get("SPLITTING_SCREEN_FLAG_VALUE"):
115
- queue_data["settings"] = {
116
- os.environ["SPLITTING_SCREEN_FLAG_NAME"]: os.environ["SPLITTING_SCREEN_FLAG_VALUE"]
117
- }
118
- else:
119
- logger.error("Splitting screen failed to update")
120
-
121
- queue: Queue = await client.create_new_queue(queue_data)
122
- return queue
123
-
124
271
  @mcp.tool(description="Update queue settings.")
125
272
  async def update_queue(queue_id: int, queue_data: dict) -> Queue | dict:
126
- """Update an existing queue with new settings."""
127
- if not is_read_write_mode():
128
- return {"error": "update_queue is not available in read-only mode"}
129
-
130
- logger.debug(f"Updating queue: queue_id={queue_id}, data={queue_data}")
131
- updated_queue_data = await client._http_client.update(Resource.Queue, queue_id, queue_data)
132
- updated_queue: Queue = client._deserializer(Resource.Queue, updated_queue_data)
133
- return updated_queue
273
+ return await _update_queue(client, queue_id, queue_data)
274
+
275
+ @mcp.tool(description="Get available queue template names for create_queue_from_template.")
276
+ async def get_queue_template_names() -> list[str]:
277
+ return list(QUEUE_TEMPLATE_NAMES)
278
+
279
+ @mcp.tool(
280
+ description="Create queue from a predefined template. Preferred method for new customer setup. "
281
+ "Templates include pre-configured schema and AI engine for common document types."
282
+ )
283
+ async def create_queue_from_template(
284
+ name: str,
285
+ template_name: QueueTemplateName,
286
+ workspace_id: int,
287
+ include_documents: bool = False,
288
+ engine_id: int | None = None,
289
+ ) -> Queue | dict:
290
+ return await _create_queue_from_template(
291
+ client, name, template_name, workspace_id, include_documents, engine_id
292
+ )
@@ -3,13 +3,10 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
- from typing import TYPE_CHECKING
6
+ from typing import TYPE_CHECKING, cast
7
7
 
8
8
  from rossum_api.domain_logic.resources import Resource
9
- from rossum_api.models.relation import (
10
- Relation, # noqa: TC002 - needed at runtime for FastMCP
11
- RelationType, # noqa: TC002 - needed at runtime for FastMCP
12
- )
9
+ from rossum_api.models.relation import Relation, RelationType
13
10
 
14
11
  if TYPE_CHECKING:
15
12
  from fastmcp import FastMCP
@@ -26,8 +23,7 @@ def register_relation_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None:
26
23
  """Retrieve relation details."""
27
24
  logger.debug(f"Retrieving relation: relation_id={relation_id}")
28
25
  relation_data = await client._http_client.fetch_one(Resource.Relation, relation_id)
29
- relation_obj: Relation = client._deserializer(Resource.Relation, relation_data)
30
- return relation_obj
26
+ return cast("Relation", client._deserializer(Resource.Relation, relation_data))
31
27
 
32
28
  @mcp.tool(
33
29
  description="List all relations with optional filters. Relations introduce common relations between annotations (edit, attachment, duplicate)."
rossum_mcp/tools/rules.py CHANGED
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import logging
6
6
  from typing import TYPE_CHECKING
7
7
 
8
- from rossum_api.models.rule import Rule # noqa: TC002 - needed at runtime for FastMCP
8
+ from rossum_api.models.rule import Rule
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from fastmcp import FastMCP
@@ -14,29 +14,40 @@ if TYPE_CHECKING:
14
14
  logger = logging.getLogger(__name__)
15
15
 
16
16
 
17
+ async def _get_rule(client: AsyncRossumAPIClient, rule_id: int) -> Rule:
18
+ logger.debug(f"Retrieving rule: rule_id={rule_id}")
19
+ rule: Rule = await client.retrieve_rule(rule_id)
20
+ return rule
21
+
22
+
23
+ async def _list_rules(
24
+ client: AsyncRossumAPIClient,
25
+ schema_id: int | None = None,
26
+ organization_id: int | None = None,
27
+ enabled: bool | None = None,
28
+ ) -> list[Rule]:
29
+ logger.debug(f"Listing rules: schema_id={schema_id}, organization_id={organization_id}, enabled={enabled}")
30
+ filters: dict = {}
31
+ if schema_id is not None:
32
+ filters["schema"] = schema_id
33
+ if organization_id is not None:
34
+ filters["organization"] = organization_id
35
+ if enabled is not None:
36
+ filters["enabled"] = enabled
37
+
38
+ rules_list: list[Rule] = [rule async for rule in client.list_rules(**filters)]
39
+ return rules_list
40
+
41
+
17
42
  def register_rule_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None:
18
43
  """Register rule-related tools with the FastMCP server."""
19
44
 
20
45
  @mcp.tool(description="Retrieve rule details.")
21
46
  async def get_rule(rule_id: int) -> Rule:
22
- """Retrieve rule details."""
23
- logger.debug(f"Retrieving rule: rule_id={rule_id}")
24
- rule: Rule = await client.retrieve_rule(rule_id)
25
- return rule
47
+ return await _get_rule(client, rule_id)
26
48
 
27
49
  @mcp.tool(description="List all rules.")
28
50
  async def list_rules(
29
51
  schema_id: int | None = None, organization_id: int | None = None, enabled: bool | None = None
30
52
  ) -> list[Rule]:
31
- """List all rules, optionally filtered by schema, organization, and enabled status."""
32
- logger.debug(f"Listing rules: schema_id={schema_id}, organization_id={organization_id}, enabled={enabled}")
33
- filters: dict = {}
34
- if schema_id is not None:
35
- filters["schema"] = schema_id
36
- if organization_id is not None:
37
- filters["organization"] = organization_id
38
- if enabled is not None:
39
- filters["enabled"] = enabled
40
-
41
- rules_list: list[Rule] = [rule async for rule in client.list_rules(**filters)]
42
- return rules_list
53
+ return await _list_rules(client, schema_id, organization_id, enabled)