rossum-mcp 1.0.1__py3-none-any.whl → 1.1.1__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 +1 -1
- rossum_mcp/server.py +18 -5
- rossum_mcp/tools/base.py +21 -2
- rossum_mcp/tools/catalog.py +3 -0
- rossum_mcp/tools/rules.py +180 -2
- rossum_mcp/tools/schemas/__init__.py +182 -0
- rossum_mcp/tools/schemas/models.py +151 -0
- rossum_mcp/tools/schemas/operations.py +183 -0
- rossum_mcp/tools/schemas/patching.py +202 -0
- rossum_mcp/tools/schemas/pruning.py +133 -0
- rossum_mcp/tools/schemas/validation.py +128 -0
- {rossum_mcp-1.0.1.dist-info → rossum_mcp-1.1.1.dist-info}/METADATA +26 -6
- rossum_mcp-1.1.1.dist-info/RECORD +30 -0
- rossum_mcp/tools/schemas.py +0 -800
- rossum_mcp-1.0.1.dist-info/RECORD +0 -25
- {rossum_mcp-1.0.1.dist-info → rossum_mcp-1.1.1.dist-info}/WHEEL +0 -0
- {rossum_mcp-1.0.1.dist-info → rossum_mcp-1.1.1.dist-info}/entry_points.txt +0 -0
- {rossum_mcp-1.0.1.dist-info → rossum_mcp-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {rossum_mcp-1.0.1.dist-info → rossum_mcp-1.1.1.dist-info}/top_level.txt +0 -0
rossum_mcp/__init__.py
CHANGED
rossum_mcp/server.py
CHANGED
|
@@ -29,6 +29,7 @@ from rossum_mcp.tools import (
|
|
|
29
29
|
register_user_tools,
|
|
30
30
|
register_workspace_tools,
|
|
31
31
|
)
|
|
32
|
+
from rossum_mcp.tools.base import get_mcp_mode, set_mcp_mode
|
|
32
33
|
|
|
33
34
|
setup_logging(app_name="rossum-mcp-server", log_level="DEBUG", use_console=False)
|
|
34
35
|
|
|
@@ -36,12 +37,8 @@ logger = logging.getLogger(__name__)
|
|
|
36
37
|
|
|
37
38
|
BASE_URL = os.environ["ROSSUM_API_BASE_URL"].rstrip("/")
|
|
38
39
|
API_TOKEN = os.environ["ROSSUM_API_TOKEN"]
|
|
39
|
-
MODE = os.environ.get("ROSSUM_MCP_MODE", "read-write").lower()
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
raise ValueError(f"Invalid ROSSUM_MCP_MODE: {MODE}. Must be 'read-only' or 'read-write'")
|
|
43
|
-
|
|
44
|
-
logger.info(f"Rossum MCP Server starting in {MODE} mode")
|
|
41
|
+
logger.info(f"Rossum MCP Server starting in {get_mcp_mode()} mode")
|
|
45
42
|
|
|
46
43
|
mcp = FastMCP("rossum-mcp-server")
|
|
47
44
|
client = AsyncRossumAPIClient(base_url=BASE_URL, credentials=Token(token=API_TOKEN))
|
|
@@ -60,6 +57,22 @@ register_user_tools(mcp, client)
|
|
|
60
57
|
register_workspace_tools(mcp, client)
|
|
61
58
|
|
|
62
59
|
|
|
60
|
+
@mcp.tool(description="Get the current MCP operation mode (read-only or read-write).")
|
|
61
|
+
async def get_mcp_mode_tool() -> dict:
|
|
62
|
+
return {"mode": get_mcp_mode()}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@mcp.tool(
|
|
66
|
+
description="Set the MCP operation mode. Use 'read-only' to disable write operations, 'read-write' to enable them."
|
|
67
|
+
)
|
|
68
|
+
async def set_mcp_mode_tool(mode: str) -> dict:
|
|
69
|
+
try:
|
|
70
|
+
set_mcp_mode(mode)
|
|
71
|
+
return {"message": f"MCP mode set to '{get_mcp_mode()}'"}
|
|
72
|
+
except ValueError as e:
|
|
73
|
+
return {"error": str(e)}
|
|
74
|
+
|
|
75
|
+
|
|
63
76
|
def main() -> None:
|
|
64
77
|
"""Main entry point for console script."""
|
|
65
78
|
mcp.run()
|
rossum_mcp/tools/base.py
CHANGED
|
@@ -13,11 +13,30 @@ if TYPE_CHECKING:
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
15
|
BASE_URL = os.environ.get("ROSSUM_API_BASE_URL", "").rstrip("/")
|
|
16
|
-
MODE = os.environ.get("ROSSUM_MCP_MODE", "read-write").lower()
|
|
17
16
|
|
|
18
17
|
# Marker used to indicate omitted fields in list responses
|
|
19
18
|
TRUNCATED_MARKER = "<omitted>"
|
|
20
19
|
|
|
20
|
+
VALID_MODES = ("read-only", "read-write")
|
|
21
|
+
|
|
22
|
+
_mcp_mode = os.environ.get("ROSSUM_MCP_MODE", "read-write").lower()
|
|
23
|
+
if _mcp_mode not in VALID_MODES:
|
|
24
|
+
raise ValueError(f"Invalid ROSSUM_MCP_MODE: {_mcp_mode}. Must be one of: {VALID_MODES}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_mcp_mode() -> str:
|
|
28
|
+
"""Return the current MCP mode."""
|
|
29
|
+
return _mcp_mode
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def set_mcp_mode(mode: str) -> None:
|
|
33
|
+
"""Set the MCP mode (case-insensitive)."""
|
|
34
|
+
global _mcp_mode
|
|
35
|
+
normalized = mode.lower()
|
|
36
|
+
if normalized not in VALID_MODES:
|
|
37
|
+
raise ValueError(f"Invalid mode '{mode}'. Must be one of: {VALID_MODES}")
|
|
38
|
+
_mcp_mode = normalized
|
|
39
|
+
|
|
21
40
|
|
|
22
41
|
def build_resource_url(resource_type: str, resource_id: int) -> str:
|
|
23
42
|
"""Build a full URL for a Rossum API resource."""
|
|
@@ -26,7 +45,7 @@ def build_resource_url(resource_type: str, resource_id: int) -> str:
|
|
|
26
45
|
|
|
27
46
|
def is_read_write_mode() -> bool:
|
|
28
47
|
"""Check if server is in read-write mode."""
|
|
29
|
-
return
|
|
48
|
+
return _mcp_mode == "read-write"
|
|
30
49
|
|
|
31
50
|
|
|
32
51
|
def truncate_dict_fields(data: dict[str, Any], fields: tuple[str, ...]) -> dict[str, Any]:
|
rossum_mcp/tools/catalog.py
CHANGED
|
@@ -139,6 +139,9 @@ TOOL_CATALOG: dict[str, ToolCategory] = {
|
|
|
139
139
|
tools=[
|
|
140
140
|
ToolInfo("get_rule", "Retrieve rule details"),
|
|
141
141
|
ToolInfo("list_rules", "List validation rules"),
|
|
142
|
+
ToolInfo("create_rule", "Create validation rule", read_only=False),
|
|
143
|
+
ToolInfo("update_rule", "Full update rule (PUT)", read_only=False),
|
|
144
|
+
ToolInfo("patch_rule", "Partial update rule (PATCH)", read_only=False),
|
|
142
145
|
ToolInfo("delete_rule", "Delete rule", read_only=False),
|
|
143
146
|
],
|
|
144
147
|
keywords=["rule", "validation", "constraint"],
|
rossum_mcp/tools/rules.py
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
6
|
+
from typing import TYPE_CHECKING, Literal, TypedDict
|
|
7
7
|
|
|
8
|
+
from rossum_api.domain_logic.resources import Resource
|
|
8
9
|
from rossum_api.models.rule import Rule
|
|
9
10
|
|
|
10
|
-
from rossum_mcp.tools.base import delete_resource
|
|
11
|
+
from rossum_mcp.tools.base import build_resource_url, delete_resource, is_read_write_mode
|
|
11
12
|
|
|
12
13
|
if TYPE_CHECKING:
|
|
13
14
|
from fastmcp import FastMCP
|
|
@@ -16,6 +17,42 @@ if TYPE_CHECKING:
|
|
|
16
17
|
logger = logging.getLogger(__name__)
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
RuleActionType = Literal[
|
|
21
|
+
"show_message",
|
|
22
|
+
"add_automation_blocker",
|
|
23
|
+
"add_validation_source",
|
|
24
|
+
"change_queue",
|
|
25
|
+
"send_email",
|
|
26
|
+
"hide_field",
|
|
27
|
+
"show_field",
|
|
28
|
+
"show_hide_field",
|
|
29
|
+
"change_status",
|
|
30
|
+
"add_label",
|
|
31
|
+
"remove_label",
|
|
32
|
+
"custom",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RuleActionPayload(TypedDict, total=False):
|
|
37
|
+
"""Payload for rule actions. Fields depend on action type."""
|
|
38
|
+
|
|
39
|
+
type: Literal["info", "warning", "error"] # for show_message
|
|
40
|
+
content: str # message content or template
|
|
41
|
+
schema_id: str # target field schema_id
|
|
42
|
+
queue_url: str # for change_queue
|
|
43
|
+
status: str # for change_status
|
|
44
|
+
label: str # for add_label/remove_label
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class RuleAction(TypedDict):
|
|
48
|
+
"""Rule action definition."""
|
|
49
|
+
|
|
50
|
+
id: str # unique action identifier
|
|
51
|
+
type: RuleActionType
|
|
52
|
+
event: Literal["validation"]
|
|
53
|
+
payload: RuleActionPayload
|
|
54
|
+
|
|
55
|
+
|
|
19
56
|
async def _get_rule(client: AsyncRossumAPIClient, rule_id: int) -> Rule:
|
|
20
57
|
logger.debug(f"Retrieving rule: rule_id={rule_id}")
|
|
21
58
|
rule: Rule = await client.retrieve_rule(rule_id)
|
|
@@ -41,6 +78,108 @@ async def _list_rules(
|
|
|
41
78
|
return rules_list
|
|
42
79
|
|
|
43
80
|
|
|
81
|
+
async def _create_rule(
|
|
82
|
+
client: AsyncRossumAPIClient,
|
|
83
|
+
name: str,
|
|
84
|
+
schema_id: int,
|
|
85
|
+
trigger_condition: str,
|
|
86
|
+
actions: list[RuleAction],
|
|
87
|
+
enabled: bool = True,
|
|
88
|
+
queue_ids: list[int] | None = None,
|
|
89
|
+
) -> Rule | dict:
|
|
90
|
+
if not is_read_write_mode():
|
|
91
|
+
return {"error": "create_rule is not available in read-only mode"}
|
|
92
|
+
|
|
93
|
+
schema_url = build_resource_url("schemas", schema_id)
|
|
94
|
+
logger.info(f"Creating rule: name={name}, schema_id={schema_id}, enabled={enabled}")
|
|
95
|
+
|
|
96
|
+
rule_data: dict = {
|
|
97
|
+
"name": name,
|
|
98
|
+
"schema": schema_url,
|
|
99
|
+
"trigger_condition": trigger_condition,
|
|
100
|
+
"actions": actions,
|
|
101
|
+
"enabled": enabled,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if queue_ids is not None:
|
|
105
|
+
rule_data["queues"] = [build_resource_url("queues", qid) for qid in queue_ids]
|
|
106
|
+
|
|
107
|
+
logger.debug(f"Rule creation payload: {rule_data}")
|
|
108
|
+
rule: Rule = await client.create_new_rule(rule_data)
|
|
109
|
+
logger.info(f"Successfully created rule: id={rule.id}, name={rule.name}")
|
|
110
|
+
return rule
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
async def _update_rule(
|
|
114
|
+
client: AsyncRossumAPIClient,
|
|
115
|
+
rule_id: int,
|
|
116
|
+
name: str,
|
|
117
|
+
trigger_condition: str,
|
|
118
|
+
actions: list[RuleAction],
|
|
119
|
+
enabled: bool,
|
|
120
|
+
queue_ids: list[int] | None = None,
|
|
121
|
+
) -> Rule | dict:
|
|
122
|
+
"""Full update (PUT) - all fields required."""
|
|
123
|
+
if not is_read_write_mode():
|
|
124
|
+
return {"error": "update_rule is not available in read-only mode"}
|
|
125
|
+
|
|
126
|
+
logger.info(f"Updating rule: rule_id={rule_id}, name={name}")
|
|
127
|
+
existing_rule: Rule = await client.retrieve_rule(rule_id)
|
|
128
|
+
|
|
129
|
+
rule_data: dict = {
|
|
130
|
+
"name": name,
|
|
131
|
+
"schema": existing_rule.schema,
|
|
132
|
+
"trigger_condition": trigger_condition,
|
|
133
|
+
"actions": actions,
|
|
134
|
+
"enabled": enabled,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if queue_ids is not None:
|
|
138
|
+
rule_data["queues"] = [build_resource_url("queues", qid) for qid in queue_ids]
|
|
139
|
+
|
|
140
|
+
logger.debug(f"Rule update payload: {rule_data}")
|
|
141
|
+
await client._http_client.update(Resource.Rule, rule_id, rule_data)
|
|
142
|
+
updated_rule: Rule = await client.retrieve_rule(rule_id)
|
|
143
|
+
logger.info(f"Successfully updated rule: id={updated_rule.id}")
|
|
144
|
+
return updated_rule
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async def _patch_rule(
|
|
148
|
+
client: AsyncRossumAPIClient,
|
|
149
|
+
rule_id: int,
|
|
150
|
+
name: str | None = None,
|
|
151
|
+
trigger_condition: str | None = None,
|
|
152
|
+
actions: list[RuleAction] | None = None,
|
|
153
|
+
enabled: bool | None = None,
|
|
154
|
+
queue_ids: list[int] | None = None,
|
|
155
|
+
) -> Rule | dict:
|
|
156
|
+
"""Partial update (PATCH) - only provided fields are updated."""
|
|
157
|
+
if not is_read_write_mode():
|
|
158
|
+
return {"error": "patch_rule is not available in read-only mode"}
|
|
159
|
+
|
|
160
|
+
logger.info(f"Patching rule: rule_id={rule_id}")
|
|
161
|
+
|
|
162
|
+
patch_data: dict = {}
|
|
163
|
+
if name is not None:
|
|
164
|
+
patch_data["name"] = name
|
|
165
|
+
if trigger_condition is not None:
|
|
166
|
+
patch_data["trigger_condition"] = trigger_condition
|
|
167
|
+
if actions is not None:
|
|
168
|
+
patch_data["actions"] = actions
|
|
169
|
+
if enabled is not None:
|
|
170
|
+
patch_data["enabled"] = enabled
|
|
171
|
+
if queue_ids is not None:
|
|
172
|
+
patch_data["queues"] = [build_resource_url("queues", qid) for qid in queue_ids]
|
|
173
|
+
|
|
174
|
+
if not patch_data:
|
|
175
|
+
return {"error": "No fields provided to update"}
|
|
176
|
+
|
|
177
|
+
logger.debug(f"Rule patch payload: {patch_data}")
|
|
178
|
+
updated_rule: Rule = await client.update_part_rule(rule_id, patch_data)
|
|
179
|
+
logger.info(f"Successfully patched rule: id={updated_rule.id}")
|
|
180
|
+
return updated_rule
|
|
181
|
+
|
|
182
|
+
|
|
44
183
|
async def _delete_rule(client: AsyncRossumAPIClient, rule_id: int) -> dict:
|
|
45
184
|
return await delete_resource("rule", rule_id, client.delete_rule)
|
|
46
185
|
|
|
@@ -58,6 +197,45 @@ def register_rule_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None:
|
|
|
58
197
|
) -> list[Rule]:
|
|
59
198
|
return await _list_rules(client, schema_id, organization_id, enabled)
|
|
60
199
|
|
|
200
|
+
@mcp.tool(
|
|
201
|
+
description="Create a new rule. Rules automate field operations based on trigger conditions (TxScript formulas like 'field.amount > 1000'). Actions require: id (unique str), type (show_message|hide_field|show_field|change_status|custom|etc), event (validation), payload (dict with type, content, schema_id for show_message). queue_ids limits rule to specific queues."
|
|
202
|
+
)
|
|
203
|
+
async def create_rule(
|
|
204
|
+
name: str,
|
|
205
|
+
schema_id: int,
|
|
206
|
+
trigger_condition: str,
|
|
207
|
+
actions: list[RuleAction],
|
|
208
|
+
enabled: bool = True,
|
|
209
|
+
queue_ids: list[int] | None = None,
|
|
210
|
+
) -> Rule | dict:
|
|
211
|
+
return await _create_rule(client, name, schema_id, trigger_condition, actions, enabled, queue_ids)
|
|
212
|
+
|
|
213
|
+
@mcp.tool(
|
|
214
|
+
description="Full update of a rule (PUT). All fields required. Use patch_rule for partial updates. queue_ids limits rule to specific queues."
|
|
215
|
+
)
|
|
216
|
+
async def update_rule(
|
|
217
|
+
rule_id: int,
|
|
218
|
+
name: str,
|
|
219
|
+
trigger_condition: str,
|
|
220
|
+
actions: list[RuleAction],
|
|
221
|
+
enabled: bool,
|
|
222
|
+
queue_ids: list[int] | None = None,
|
|
223
|
+
) -> Rule | dict:
|
|
224
|
+
return await _update_rule(client, rule_id, name, trigger_condition, actions, enabled, queue_ids)
|
|
225
|
+
|
|
226
|
+
@mcp.tool(
|
|
227
|
+
description="Partial update of a rule (PATCH). Only provided fields are updated. queue_ids limits rule to specific queues (empty list removes all)."
|
|
228
|
+
)
|
|
229
|
+
async def patch_rule(
|
|
230
|
+
rule_id: int,
|
|
231
|
+
name: str | None = None,
|
|
232
|
+
trigger_condition: str | None = None,
|
|
233
|
+
actions: list[RuleAction] | None = None,
|
|
234
|
+
enabled: bool | None = None,
|
|
235
|
+
queue_ids: list[int] | None = None,
|
|
236
|
+
) -> Rule | dict:
|
|
237
|
+
return await _patch_rule(client, rule_id, name, trigger_condition, actions, enabled, queue_ids)
|
|
238
|
+
|
|
61
239
|
@mcp.tool(description="Delete a rule.")
|
|
62
240
|
async def delete_rule(rule_id: int) -> dict:
|
|
63
241
|
return await _delete_rule(client, rule_id)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Schema tools for Rossum MCP Server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from rossum_api.models.schema import Schema
|
|
8
|
+
|
|
9
|
+
from rossum_mcp.tools.schemas.models import (
|
|
10
|
+
DatapointType,
|
|
11
|
+
NodeCategory,
|
|
12
|
+
SchemaDatapoint,
|
|
13
|
+
SchemaMultivalue,
|
|
14
|
+
SchemaNode,
|
|
15
|
+
SchemaNodeUpdate,
|
|
16
|
+
SchemaTreeNode,
|
|
17
|
+
SchemaTuple,
|
|
18
|
+
)
|
|
19
|
+
from rossum_mcp.tools.schemas.operations import (
|
|
20
|
+
create_schema,
|
|
21
|
+
delete_schema,
|
|
22
|
+
get_schema,
|
|
23
|
+
get_schema_tree_structure,
|
|
24
|
+
list_schemas,
|
|
25
|
+
patch_schema,
|
|
26
|
+
prune_schema_fields,
|
|
27
|
+
update_schema,
|
|
28
|
+
)
|
|
29
|
+
from rossum_mcp.tools.schemas.patching import (
|
|
30
|
+
PatchOperation,
|
|
31
|
+
_apply_add_operation,
|
|
32
|
+
_apply_remove_operation,
|
|
33
|
+
_apply_update_operation,
|
|
34
|
+
_find_node_anywhere,
|
|
35
|
+
_find_node_in_children,
|
|
36
|
+
_find_parent_children_list,
|
|
37
|
+
_get_section_children_as_list,
|
|
38
|
+
apply_schema_patch,
|
|
39
|
+
)
|
|
40
|
+
from rossum_mcp.tools.schemas.pruning import (
|
|
41
|
+
_collect_all_field_ids,
|
|
42
|
+
_collect_ancestor_ids,
|
|
43
|
+
_extract_schema_tree,
|
|
44
|
+
_remove_fields_from_content,
|
|
45
|
+
)
|
|
46
|
+
from rossum_mcp.tools.schemas.validation import (
|
|
47
|
+
MAX_ID_LENGTH,
|
|
48
|
+
VALID_DATAPOINT_TYPES,
|
|
49
|
+
VALID_UI_CONFIGURATION_EDIT,
|
|
50
|
+
VALID_UI_CONFIGURATION_TYPES,
|
|
51
|
+
SchemaValidationError,
|
|
52
|
+
_validate_datapoint,
|
|
53
|
+
_validate_id,
|
|
54
|
+
_validate_multivalue,
|
|
55
|
+
_validate_node,
|
|
56
|
+
_validate_section,
|
|
57
|
+
_validate_tuple,
|
|
58
|
+
sanitize_schema_content,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if TYPE_CHECKING:
|
|
62
|
+
from fastmcp import FastMCP
|
|
63
|
+
from rossum_api import AsyncRossumAPIClient
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
"MAX_ID_LENGTH",
|
|
67
|
+
"VALID_DATAPOINT_TYPES",
|
|
68
|
+
"VALID_UI_CONFIGURATION_EDIT",
|
|
69
|
+
"VALID_UI_CONFIGURATION_TYPES",
|
|
70
|
+
"DatapointType",
|
|
71
|
+
"NodeCategory",
|
|
72
|
+
"PatchOperation",
|
|
73
|
+
"SchemaDatapoint",
|
|
74
|
+
"SchemaMultivalue",
|
|
75
|
+
"SchemaNode",
|
|
76
|
+
"SchemaNodeUpdate",
|
|
77
|
+
"SchemaTreeNode",
|
|
78
|
+
"SchemaTuple",
|
|
79
|
+
"SchemaValidationError",
|
|
80
|
+
"_apply_add_operation",
|
|
81
|
+
"_apply_remove_operation",
|
|
82
|
+
"_apply_update_operation",
|
|
83
|
+
"_collect_all_field_ids",
|
|
84
|
+
"_collect_ancestor_ids",
|
|
85
|
+
"_extract_schema_tree",
|
|
86
|
+
"_find_node_anywhere",
|
|
87
|
+
"_find_node_in_children",
|
|
88
|
+
"_find_parent_children_list",
|
|
89
|
+
"_get_section_children_as_list",
|
|
90
|
+
"_remove_fields_from_content",
|
|
91
|
+
"_validate_datapoint",
|
|
92
|
+
"_validate_id",
|
|
93
|
+
"_validate_multivalue",
|
|
94
|
+
"_validate_node",
|
|
95
|
+
"_validate_section",
|
|
96
|
+
"_validate_tuple",
|
|
97
|
+
"apply_schema_patch",
|
|
98
|
+
"create_schema",
|
|
99
|
+
"delete_schema",
|
|
100
|
+
"get_schema",
|
|
101
|
+
"get_schema_tree_structure",
|
|
102
|
+
"list_schemas",
|
|
103
|
+
"patch_schema",
|
|
104
|
+
"prune_schema_fields",
|
|
105
|
+
"register_schema_tools",
|
|
106
|
+
"sanitize_schema_content",
|
|
107
|
+
"update_schema",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def register_schema_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None:
|
|
112
|
+
"""Register schema-related tools with the FastMCP server."""
|
|
113
|
+
from rossum_mcp.tools.schemas import operations as ops # noqa: PLC0415 - avoid circular import
|
|
114
|
+
|
|
115
|
+
@mcp.tool(description="Retrieve schema details.")
|
|
116
|
+
async def get_schema(schema_id: int) -> Schema | dict:
|
|
117
|
+
return await ops.get_schema(client, schema_id)
|
|
118
|
+
|
|
119
|
+
@mcp.tool(description="List all schemas with optional filters.")
|
|
120
|
+
async def list_schemas(name: str | None = None, queue_id: int | None = None) -> list[Schema]:
|
|
121
|
+
return await ops.list_schemas(client, name, queue_id)
|
|
122
|
+
|
|
123
|
+
@mcp.tool(description="Update schema, typically for field-level thresholds.")
|
|
124
|
+
async def update_schema(schema_id: int, schema_data: dict) -> Schema | dict:
|
|
125
|
+
return await ops.update_schema(client, schema_id, schema_data)
|
|
126
|
+
|
|
127
|
+
@mcp.tool(description="Create a schema. Must have ≥1 section with children (datapoints).")
|
|
128
|
+
async def create_schema(name: str, content: list[dict]) -> Schema | dict:
|
|
129
|
+
return await ops.create_schema(client, name, content)
|
|
130
|
+
|
|
131
|
+
@mcp.tool(
|
|
132
|
+
description="""Patch schema nodes (add/update/remove fields in a schema).
|
|
133
|
+
|
|
134
|
+
You MUST load `schema-patching` skill first to avoid errors.
|
|
135
|
+
|
|
136
|
+
Operations:
|
|
137
|
+
- add: Create new field. Requires parent_id (section or tuple id) and node_data.
|
|
138
|
+
- update: Modify existing field. Requires node_data with fields to change.
|
|
139
|
+
- remove: Delete field. Only requires node_id.
|
|
140
|
+
|
|
141
|
+
Node types for add:
|
|
142
|
+
- Datapoint (simple field): {"label": "Field Name", "category": "datapoint", "type": "string|number|date|enum"}
|
|
143
|
+
- Enum field: Include "options": [{"value": "v1", "label": "Label 1"}, ...]
|
|
144
|
+
- Multivalue (table): {"label": "Table", "category": "multivalue", "children": <tuple>}
|
|
145
|
+
- Tuple (table row): {"id": "row_id", "label": "Row", "category": "tuple", "children": [<datapoints with id>]}
|
|
146
|
+
|
|
147
|
+
Important: Datapoints inside a tuple MUST have an "id" field. Section-level datapoints get id from node_id parameter.
|
|
148
|
+
"""
|
|
149
|
+
)
|
|
150
|
+
async def patch_schema(
|
|
151
|
+
schema_id: int,
|
|
152
|
+
operation: PatchOperation,
|
|
153
|
+
node_id: str,
|
|
154
|
+
node_data: SchemaNode | SchemaNodeUpdate | None = None,
|
|
155
|
+
parent_id: str | None = None,
|
|
156
|
+
position: int | None = None,
|
|
157
|
+
) -> Schema | dict:
|
|
158
|
+
return await ops.patch_schema(client, schema_id, operation, node_id, node_data, parent_id, position)
|
|
159
|
+
|
|
160
|
+
@mcp.tool(description="Get lightweight tree structure of schema with only ids, labels, categories, and types.")
|
|
161
|
+
async def get_schema_tree_structure(schema_id: int) -> list[dict] | dict:
|
|
162
|
+
return await ops.get_schema_tree_structure(client, schema_id)
|
|
163
|
+
|
|
164
|
+
@mcp.tool(
|
|
165
|
+
description="""Remove multiple fields from schema at once. Efficient for pruning unwanted fields during setup.
|
|
166
|
+
|
|
167
|
+
Use fields_to_keep OR fields_to_remove (not both):
|
|
168
|
+
- fields_to_keep: Keep only these field IDs (plus sections). All others removed.
|
|
169
|
+
- fields_to_remove: Remove these specific field IDs.
|
|
170
|
+
|
|
171
|
+
Returns dict with removed_fields and remaining_fields lists. Sections cannot be removed."""
|
|
172
|
+
)
|
|
173
|
+
async def prune_schema_fields(
|
|
174
|
+
schema_id: int,
|
|
175
|
+
fields_to_keep: list[str] | None = None,
|
|
176
|
+
fields_to_remove: list[str] | None = None,
|
|
177
|
+
) -> dict:
|
|
178
|
+
return await ops.prune_schema_fields(client, schema_id, fields_to_keep, fields_to_remove)
|
|
179
|
+
|
|
180
|
+
@mcp.tool(description="Delete a schema. Fails if schema is linked to a queue or annotation (HTTP 409).")
|
|
181
|
+
async def delete_schema(schema_id: int) -> dict:
|
|
182
|
+
return await ops.delete_schema(client, schema_id)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Schema dataclass models for Rossum MCP Server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
DatapointType = Literal["string", "number", "date", "enum", "button"]
|
|
9
|
+
NodeCategory = Literal["datapoint", "multivalue", "tuple"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class SchemaDatapoint:
|
|
14
|
+
"""A datapoint node for schema patch operations.
|
|
15
|
+
|
|
16
|
+
Use for adding/updating fields that capture or display values.
|
|
17
|
+
When used inside a tuple (table), id is required.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
label: str
|
|
21
|
+
id: str | None = None
|
|
22
|
+
category: Literal["datapoint"] = "datapoint"
|
|
23
|
+
type: DatapointType | None = None
|
|
24
|
+
rir_field_names: list[str] | None = None
|
|
25
|
+
default_value: str | None = None
|
|
26
|
+
score_threshold: float | None = None
|
|
27
|
+
hidden: bool = False
|
|
28
|
+
disable_prediction: bool = False
|
|
29
|
+
can_export: bool = True
|
|
30
|
+
constraints: dict | None = None
|
|
31
|
+
options: list[dict] | None = None
|
|
32
|
+
ui_configuration: dict | None = None
|
|
33
|
+
formula: str | None = None
|
|
34
|
+
prompt: str | None = None
|
|
35
|
+
context: list[str] | None = None
|
|
36
|
+
width: int | None = None
|
|
37
|
+
stretch: bool | None = None
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> dict:
|
|
40
|
+
"""Convert to dict, excluding None values."""
|
|
41
|
+
return {k: v for k, v in asdict(self).items() if v is not None}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class SchemaTuple:
|
|
46
|
+
"""A tuple node for schema patch operations.
|
|
47
|
+
|
|
48
|
+
Use within multivalue to define table row structure with multiple columns.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
id: str
|
|
52
|
+
label: str
|
|
53
|
+
children: list[SchemaDatapoint]
|
|
54
|
+
category: Literal["tuple"] = "tuple"
|
|
55
|
+
hidden: bool = False
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict:
|
|
58
|
+
"""Convert to dict, excluding None values."""
|
|
59
|
+
result: dict = {"id": self.id, "category": self.category, "label": self.label}
|
|
60
|
+
if self.hidden:
|
|
61
|
+
result["hidden"] = self.hidden
|
|
62
|
+
result["children"] = [child.to_dict() for child in self.children]
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class SchemaMultivalue:
|
|
68
|
+
"""A multivalue node for schema patch operations.
|
|
69
|
+
|
|
70
|
+
Use for repeating fields or tables. Children is a single Tuple or Datapoint (NOT a list).
|
|
71
|
+
The id is optional here since it gets set from node_id in patch_schema.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
label: str
|
|
75
|
+
children: SchemaTuple | SchemaDatapoint
|
|
76
|
+
id: str | None = None
|
|
77
|
+
category: Literal["multivalue"] = "multivalue"
|
|
78
|
+
rir_field_names: list[str] | None = None
|
|
79
|
+
min_occurrences: int | None = None
|
|
80
|
+
max_occurrences: int | None = None
|
|
81
|
+
hidden: bool = False
|
|
82
|
+
|
|
83
|
+
def to_dict(self) -> dict:
|
|
84
|
+
"""Convert to dict, excluding None values."""
|
|
85
|
+
result: dict = {"label": self.label, "category": self.category}
|
|
86
|
+
if self.id:
|
|
87
|
+
result["id"] = self.id
|
|
88
|
+
if self.rir_field_names:
|
|
89
|
+
result["rir_field_names"] = self.rir_field_names
|
|
90
|
+
if self.min_occurrences is not None:
|
|
91
|
+
result["min_occurrences"] = self.min_occurrences
|
|
92
|
+
if self.max_occurrences is not None:
|
|
93
|
+
result["max_occurrences"] = self.max_occurrences
|
|
94
|
+
if self.hidden:
|
|
95
|
+
result["hidden"] = self.hidden
|
|
96
|
+
result["children"] = self.children.to_dict()
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class SchemaNodeUpdate:
|
|
102
|
+
"""Partial update for an existing schema node.
|
|
103
|
+
|
|
104
|
+
Only include fields you want to update - all fields are optional.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
label: str | None = None
|
|
108
|
+
type: DatapointType | None = None
|
|
109
|
+
score_threshold: float | None = None
|
|
110
|
+
hidden: bool | None = None
|
|
111
|
+
disable_prediction: bool | None = None
|
|
112
|
+
can_export: bool | None = None
|
|
113
|
+
default_value: str | None = None
|
|
114
|
+
rir_field_names: list[str] | None = None
|
|
115
|
+
constraints: dict | None = None
|
|
116
|
+
options: list[dict] | None = None
|
|
117
|
+
ui_configuration: dict | None = None
|
|
118
|
+
formula: str | None = None
|
|
119
|
+
prompt: str | None = None
|
|
120
|
+
context: list[str] | None = None
|
|
121
|
+
width: int | None = None
|
|
122
|
+
stretch: bool | None = None
|
|
123
|
+
min_occurrences: int | None = None
|
|
124
|
+
max_occurrences: int | None = None
|
|
125
|
+
|
|
126
|
+
def to_dict(self) -> dict:
|
|
127
|
+
"""Convert to dict, excluding None values."""
|
|
128
|
+
return {k: v for k, v in asdict(self).items() if v is not None}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class SchemaTreeNode:
|
|
133
|
+
"""Lightweight schema node for tree structure display."""
|
|
134
|
+
|
|
135
|
+
id: str
|
|
136
|
+
label: str
|
|
137
|
+
category: str
|
|
138
|
+
type: str | None = None
|
|
139
|
+
children: list[SchemaTreeNode] | None = None
|
|
140
|
+
|
|
141
|
+
def to_dict(self) -> dict:
|
|
142
|
+
"""Convert to dict, excluding None values."""
|
|
143
|
+
result: dict = {"id": self.id, "label": self.label, "category": self.category}
|
|
144
|
+
if self.type:
|
|
145
|
+
result["type"] = self.type
|
|
146
|
+
if self.children:
|
|
147
|
+
result["children"] = [child.to_dict() for child in self.children]
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
SchemaNode = SchemaDatapoint | SchemaMultivalue | SchemaTuple
|