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
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import logging
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, Literal
|
|
7
|
+
|
|
8
|
+
from rossum_api.domain_logic.resources import Resource
|
|
9
|
+
from rossum_api.models.schema import Schema # noqa: TC002 - needed at runtime for FastMCP
|
|
10
|
+
|
|
11
|
+
from rossum_mcp.tools.base import is_read_write_mode
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from fastmcp import FastMCP
|
|
15
|
+
from rossum_api import AsyncRossumAPIClient
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
PatchOperation = Literal["add", "update", "remove"]
|
|
20
|
+
DatapointType = Literal["string", "number", "date", "enum", "button"]
|
|
21
|
+
NodeCategory = Literal["datapoint", "multivalue", "tuple"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class SchemaDatapoint:
|
|
26
|
+
"""A datapoint node for schema patch operations.
|
|
27
|
+
|
|
28
|
+
Use for adding/updating fields that capture or display values.
|
|
29
|
+
When used inside a tuple (table), id is required.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
label: str
|
|
33
|
+
id: str | None = None
|
|
34
|
+
category: Literal["datapoint"] = "datapoint"
|
|
35
|
+
type: DatapointType | None = None
|
|
36
|
+
rir_field_names: list[str] | None = None
|
|
37
|
+
default_value: str | None = None
|
|
38
|
+
score_threshold: float | None = None
|
|
39
|
+
hidden: bool = False
|
|
40
|
+
disable_prediction: bool = False
|
|
41
|
+
can_export: bool = True
|
|
42
|
+
constraints: dict | None = None
|
|
43
|
+
options: list[dict] | None = None
|
|
44
|
+
ui_configuration: dict | None = None
|
|
45
|
+
formula: str | None = None
|
|
46
|
+
prompt: str | None = None
|
|
47
|
+
context: list[str] | None = None
|
|
48
|
+
width: int | None = None
|
|
49
|
+
stretch: bool | None = None
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> dict:
|
|
52
|
+
"""Convert to dict, excluding None values."""
|
|
53
|
+
return {k: v for k, v in asdict(self).items() if v is not None}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class SchemaTuple:
|
|
58
|
+
"""A tuple node for schema patch operations.
|
|
59
|
+
|
|
60
|
+
Use within multivalue to define table row structure with multiple columns.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
id: str
|
|
64
|
+
label: str
|
|
65
|
+
children: list[SchemaDatapoint]
|
|
66
|
+
category: Literal["tuple"] = "tuple"
|
|
67
|
+
hidden: bool = False
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict:
|
|
70
|
+
"""Convert to dict, excluding None values."""
|
|
71
|
+
result: dict = {"id": self.id, "category": self.category, "label": self.label}
|
|
72
|
+
if self.hidden:
|
|
73
|
+
result["hidden"] = self.hidden
|
|
74
|
+
result["children"] = [child.to_dict() for child in self.children]
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class SchemaMultivalue:
|
|
80
|
+
"""A multivalue node for schema patch operations.
|
|
81
|
+
|
|
82
|
+
Use for repeating fields or tables. Children is a single Tuple or Datapoint (NOT a list).
|
|
83
|
+
The id is optional here since it gets set from node_id in patch_schema.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
label: str
|
|
87
|
+
children: SchemaTuple | SchemaDatapoint
|
|
88
|
+
id: str | None = None
|
|
89
|
+
category: Literal["multivalue"] = "multivalue"
|
|
90
|
+
rir_field_names: list[str] | None = None
|
|
91
|
+
min_occurrences: int | None = None
|
|
92
|
+
max_occurrences: int | None = None
|
|
93
|
+
hidden: bool = False
|
|
94
|
+
|
|
95
|
+
def to_dict(self) -> dict:
|
|
96
|
+
"""Convert to dict, excluding None values."""
|
|
97
|
+
result: dict = {"label": self.label, "category": self.category}
|
|
98
|
+
if self.id:
|
|
99
|
+
result["id"] = self.id
|
|
100
|
+
if self.rir_field_names:
|
|
101
|
+
result["rir_field_names"] = self.rir_field_names
|
|
102
|
+
if self.min_occurrences is not None:
|
|
103
|
+
result["min_occurrences"] = self.min_occurrences
|
|
104
|
+
if self.max_occurrences is not None:
|
|
105
|
+
result["max_occurrences"] = self.max_occurrences
|
|
106
|
+
if self.hidden:
|
|
107
|
+
result["hidden"] = self.hidden
|
|
108
|
+
result["children"] = self.children.to_dict()
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class SchemaNodeUpdate:
|
|
114
|
+
"""Partial update for an existing schema node.
|
|
115
|
+
|
|
116
|
+
Only include fields you want to update - all fields are optional.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
label: str | None = None
|
|
120
|
+
type: DatapointType | None = None
|
|
121
|
+
score_threshold: float | None = None
|
|
122
|
+
hidden: bool | None = None
|
|
123
|
+
disable_prediction: bool | None = None
|
|
124
|
+
can_export: bool | None = None
|
|
125
|
+
default_value: str | None = None
|
|
126
|
+
rir_field_names: list[str] | None = None
|
|
127
|
+
constraints: dict | None = None
|
|
128
|
+
options: list[dict] | None = None
|
|
129
|
+
ui_configuration: dict | None = None
|
|
130
|
+
formula: str | None = None
|
|
131
|
+
prompt: str | None = None
|
|
132
|
+
context: list[str] | None = None
|
|
133
|
+
width: int | None = None
|
|
134
|
+
stretch: bool | None = None
|
|
135
|
+
min_occurrences: int | None = None
|
|
136
|
+
max_occurrences: int | None = None
|
|
137
|
+
|
|
138
|
+
def to_dict(self) -> dict:
|
|
139
|
+
"""Convert to dict, excluding None values."""
|
|
140
|
+
return {k: v for k, v in asdict(self).items() if v is not None}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
SchemaNode = SchemaDatapoint | SchemaMultivalue | SchemaTuple
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _find_node_in_children(
|
|
147
|
+
children: list[dict], node_id: str, parent_node: dict | None = None
|
|
148
|
+
) -> tuple[dict | None, int | None, list[dict] | None, dict | None]:
|
|
149
|
+
"""Recursively find a node by ID in schema children.
|
|
150
|
+
|
|
151
|
+
Returns (node, index, parent_children_list, parent_node) or (None, None, None, None) if not found.
|
|
152
|
+
The parent_node is needed for multivalue's dict children where we need to modify the parent directly.
|
|
153
|
+
"""
|
|
154
|
+
for i, child in enumerate(children):
|
|
155
|
+
if child.get("id") == node_id:
|
|
156
|
+
return child, i, children, parent_node
|
|
157
|
+
|
|
158
|
+
nested_children = child.get("children")
|
|
159
|
+
if nested_children:
|
|
160
|
+
if isinstance(nested_children, list):
|
|
161
|
+
result = _find_node_in_children(nested_children, node_id, child)
|
|
162
|
+
if result[0] is not None:
|
|
163
|
+
return result
|
|
164
|
+
elif isinstance(nested_children, dict):
|
|
165
|
+
# Multivalue has a single dict child (tuple), not a list
|
|
166
|
+
if nested_children.get("id") == node_id:
|
|
167
|
+
# Return the parent node so caller can modify child["children"] directly
|
|
168
|
+
return nested_children, 0, None, child
|
|
169
|
+
if "children" in nested_children:
|
|
170
|
+
result = _find_node_in_children(nested_children["children"], node_id, nested_children)
|
|
171
|
+
if result[0] is not None:
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
return None, None, None, None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _find_parent_children_list(content: list[dict], parent_id: str) -> list[dict] | None:
|
|
178
|
+
"""Find the children list of a parent node by its ID."""
|
|
179
|
+
for section in content:
|
|
180
|
+
if section.get("id") == parent_id:
|
|
181
|
+
children: list[dict] = section.setdefault("children", [])
|
|
182
|
+
return children
|
|
183
|
+
|
|
184
|
+
section_children = section.get("children", [])
|
|
185
|
+
node, _, _, _ = _find_node_in_children(section_children, parent_id)
|
|
186
|
+
if node is not None:
|
|
187
|
+
if "children" in node:
|
|
188
|
+
if isinstance(node["children"], list):
|
|
189
|
+
result: list[dict] = node["children"]
|
|
190
|
+
return result
|
|
191
|
+
if isinstance(node["children"], dict):
|
|
192
|
+
return [node["children"]]
|
|
193
|
+
else:
|
|
194
|
+
node["children"] = []
|
|
195
|
+
node_children: list[dict] = node["children"]
|
|
196
|
+
return node_children
|
|
197
|
+
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _apply_add_operation(
|
|
202
|
+
content: list[dict], node_id: str, node_data: dict | None, parent_id: str | None, position: int | None
|
|
203
|
+
) -> list[dict]:
|
|
204
|
+
if node_data is None:
|
|
205
|
+
raise ValueError("node_data is required for 'add' operation")
|
|
206
|
+
if parent_id is None:
|
|
207
|
+
raise ValueError("parent_id is required for 'add' operation")
|
|
208
|
+
|
|
209
|
+
node_data = copy.deepcopy(node_data)
|
|
210
|
+
node_data["id"] = node_id
|
|
211
|
+
|
|
212
|
+
parent_children = _find_parent_children_list(content, parent_id)
|
|
213
|
+
if parent_children is None:
|
|
214
|
+
raise ValueError(f"Parent node '{parent_id}' not found in schema")
|
|
215
|
+
|
|
216
|
+
if position is not None and 0 <= position <= len(parent_children):
|
|
217
|
+
parent_children.insert(position, node_data)
|
|
218
|
+
else:
|
|
219
|
+
parent_children.append(node_data)
|
|
220
|
+
return content
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _apply_update_operation(content: list[dict], node_id: str, node_data: dict | None) -> list[dict]:
|
|
224
|
+
if node_data is None:
|
|
225
|
+
raise ValueError("node_data is required for 'update' operation")
|
|
226
|
+
|
|
227
|
+
for section in content:
|
|
228
|
+
if section.get("id") == node_id:
|
|
229
|
+
section.update(node_data)
|
|
230
|
+
return content
|
|
231
|
+
|
|
232
|
+
node: dict | None = None
|
|
233
|
+
for section in content:
|
|
234
|
+
node, _, _, _ = _find_node_in_children(section.get("children", []), node_id)
|
|
235
|
+
if node is not None:
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
if node is None:
|
|
239
|
+
raise ValueError(f"Node '{node_id}' not found in schema")
|
|
240
|
+
|
|
241
|
+
node.update(node_data)
|
|
242
|
+
return content
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _apply_remove_operation(content: list[dict], node_id: str) -> list[dict]:
|
|
246
|
+
for section in content:
|
|
247
|
+
if section.get("id") == node_id:
|
|
248
|
+
raise ValueError("Cannot remove a section - sections must exist")
|
|
249
|
+
|
|
250
|
+
for section in content:
|
|
251
|
+
section_children = section.get("children", [])
|
|
252
|
+
node, idx, parent_list, parent_node = _find_node_in_children(section_children, node_id)
|
|
253
|
+
if node is not None and idx is not None:
|
|
254
|
+
if parent_list is not None:
|
|
255
|
+
# Node is in a regular list of children
|
|
256
|
+
parent_list.pop(idx)
|
|
257
|
+
elif parent_node is not None:
|
|
258
|
+
# Node is multivalue's single dict child - cannot remove tuple from multivalue
|
|
259
|
+
raise ValueError(f"Cannot remove tuple '{node_id}' from multivalue - remove the multivalue instead")
|
|
260
|
+
return content
|
|
261
|
+
|
|
262
|
+
raise ValueError(f"Node '{node_id}' not found in schema")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def apply_schema_patch(
|
|
266
|
+
content: list[dict],
|
|
267
|
+
operation: PatchOperation,
|
|
268
|
+
node_id: str,
|
|
269
|
+
node_data: dict | None = None,
|
|
270
|
+
parent_id: str | None = None,
|
|
271
|
+
position: int | None = None,
|
|
272
|
+
) -> list[dict]:
|
|
273
|
+
"""Apply a patch operation to schema content.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
content: The schema content (list of sections)
|
|
277
|
+
operation: One of "add", "update", "remove"
|
|
278
|
+
node_id: ID of the node to operate on
|
|
279
|
+
node_data: Data for add/update operations
|
|
280
|
+
parent_id: Parent node ID for add operation (section ID or multivalue/tuple ID)
|
|
281
|
+
position: Optional position for add operation (appends if not specified)
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Modified content
|
|
285
|
+
"""
|
|
286
|
+
content = copy.deepcopy(content)
|
|
287
|
+
|
|
288
|
+
if operation == "add":
|
|
289
|
+
return _apply_add_operation(content, node_id, node_data, parent_id, position)
|
|
290
|
+
if operation == "update":
|
|
291
|
+
return _apply_update_operation(content, node_id, node_data)
|
|
292
|
+
if operation == "remove":
|
|
293
|
+
return _apply_remove_operation(content, node_id)
|
|
294
|
+
|
|
295
|
+
return content
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def register_schema_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None:
|
|
299
|
+
"""Register schema-related tools with the FastMCP server."""
|
|
300
|
+
|
|
301
|
+
@mcp.tool(description="Retrieve schema details.")
|
|
302
|
+
async def get_schema(schema_id: int) -> Schema:
|
|
303
|
+
"""Retrieve schema details."""
|
|
304
|
+
schema: Schema = await client.retrieve_schema(schema_id)
|
|
305
|
+
return schema
|
|
306
|
+
|
|
307
|
+
@mcp.tool(description="Update schema, typically for field-level thresholds.")
|
|
308
|
+
async def update_schema(schema_id: int, schema_data: dict) -> Schema | dict:
|
|
309
|
+
"""Update an existing schema."""
|
|
310
|
+
if not is_read_write_mode():
|
|
311
|
+
return {"error": "update_schema is not available in read-only mode"}
|
|
312
|
+
|
|
313
|
+
logger.debug(f"Updating schema: schema_id={schema_id}")
|
|
314
|
+
await client._http_client.update(Resource.Schema, schema_id, schema_data)
|
|
315
|
+
updated_schema: Schema = await client.retrieve_schema(schema_id)
|
|
316
|
+
return updated_schema
|
|
317
|
+
|
|
318
|
+
@mcp.tool(description="Create a schema. Must have ≥1 section with children (datapoints).")
|
|
319
|
+
async def create_schema(name: str, content: list[dict]) -> Schema | dict:
|
|
320
|
+
"""Create a new schema."""
|
|
321
|
+
if not is_read_write_mode():
|
|
322
|
+
return {"error": "create_schema is not available in read-only mode"}
|
|
323
|
+
|
|
324
|
+
logger.debug(f"Creating schema: name={name}")
|
|
325
|
+
schema_data = {"name": name, "content": content}
|
|
326
|
+
schema: Schema = await client.create_new_schema(schema_data)
|
|
327
|
+
return schema
|
|
328
|
+
|
|
329
|
+
@mcp.tool(
|
|
330
|
+
description="""Patch schema nodes (add/update/remove fields in a schema).
|
|
331
|
+
|
|
332
|
+
Operations:
|
|
333
|
+
- add: Create new field. Requires parent_id (section or tuple id) and node_data.
|
|
334
|
+
- update: Modify existing field. Requires node_data with fields to change.
|
|
335
|
+
- remove: Delete field. Only requires node_id.
|
|
336
|
+
|
|
337
|
+
Node types for add:
|
|
338
|
+
- Datapoint (simple field): {"label": "Field Name", "category": "datapoint", "type": "string|number|date|enum"}
|
|
339
|
+
- Enum field: Include "options": [{"value": "v1", "label": "Label 1"}, ...]
|
|
340
|
+
- Multivalue (table): {"label": "Table", "category": "multivalue", "children": <tuple>}
|
|
341
|
+
- Tuple (table row): {"id": "row_id", "label": "Row", "category": "tuple", "children": [<datapoints with id>]}
|
|
342
|
+
|
|
343
|
+
Important: Datapoints inside a tuple MUST have an "id" field. Section-level datapoints get id from node_id parameter."""
|
|
344
|
+
)
|
|
345
|
+
async def patch_schema(
|
|
346
|
+
schema_id: int,
|
|
347
|
+
operation: PatchOperation,
|
|
348
|
+
node_id: str,
|
|
349
|
+
node_data: SchemaNode | SchemaNodeUpdate | None = None,
|
|
350
|
+
parent_id: str | None = None,
|
|
351
|
+
position: int | None = None,
|
|
352
|
+
) -> Schema | dict:
|
|
353
|
+
if not is_read_write_mode():
|
|
354
|
+
return {"error": "patch_schema is not available in read-only mode"}
|
|
355
|
+
|
|
356
|
+
if operation not in ("add", "update", "remove"):
|
|
357
|
+
return {"error": f"Invalid operation '{operation}'. Must be 'add', 'update', or 'remove'."}
|
|
358
|
+
|
|
359
|
+
logger.debug(f"Patching schema: schema_id={schema_id}, operation={operation}, node_id={node_id}")
|
|
360
|
+
|
|
361
|
+
node_data_dict: dict | None = None
|
|
362
|
+
if node_data is not None:
|
|
363
|
+
node_data_dict = node_data.to_dict() if hasattr(node_data, "to_dict") else dict(node_data) # type: ignore[call-overload]
|
|
364
|
+
|
|
365
|
+
current_schema: dict = await client._http_client.request_json("GET", f"schemas/{schema_id}")
|
|
366
|
+
content_list = current_schema.get("content", [])
|
|
367
|
+
if not isinstance(content_list, list):
|
|
368
|
+
return {"error": "Unexpected schema content format"}
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
patched_content = apply_schema_patch(
|
|
372
|
+
content=content_list,
|
|
373
|
+
operation=operation, # type: ignore[arg-type]
|
|
374
|
+
node_id=node_id,
|
|
375
|
+
node_data=node_data_dict,
|
|
376
|
+
parent_id=parent_id,
|
|
377
|
+
position=position,
|
|
378
|
+
)
|
|
379
|
+
except ValueError as e:
|
|
380
|
+
return {"error": str(e)}
|
|
381
|
+
|
|
382
|
+
await client._http_client.update(Resource.Schema, schema_id, {"content": patched_content})
|
|
383
|
+
updated_schema: Schema = await client.retrieve_schema(schema_id)
|
|
384
|
+
return updated_schema
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""User 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.group import Group # noqa: TC002 - needed at runtime for FastMCP
|
|
9
|
+
from rossum_api.models.user import User # noqa: TC002 - needed at runtime for FastMCP
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from fastmcp import FastMCP
|
|
13
|
+
from rossum_api import AsyncRossumAPIClient
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def register_user_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None:
|
|
19
|
+
@mcp.tool(description="Retrieve a single user by ID. Use list_users first to find users by username/email.")
|
|
20
|
+
async def get_user(user_id: int) -> User:
|
|
21
|
+
user: User = await client.retrieve_user(user_id)
|
|
22
|
+
return user
|
|
23
|
+
|
|
24
|
+
@mcp.tool(
|
|
25
|
+
description="List users. Filter by username/email to find specific users. Beware that users with 'organization_group_admin' role are special, e.g. cannot be used as token owners; you can filter them out with `is_organization_group_admin=False`."
|
|
26
|
+
)
|
|
27
|
+
async def list_users(
|
|
28
|
+
username: str | None = None,
|
|
29
|
+
email: str | None = None,
|
|
30
|
+
first_name: str | None = None,
|
|
31
|
+
last_name: str | None = None,
|
|
32
|
+
is_active: bool | None = None,
|
|
33
|
+
is_organization_group_admin: bool | None = None,
|
|
34
|
+
) -> list[User]:
|
|
35
|
+
filter_mapping: dict = {
|
|
36
|
+
"username": username,
|
|
37
|
+
"email": email,
|
|
38
|
+
"first_name": first_name,
|
|
39
|
+
"last_name": last_name,
|
|
40
|
+
"is_active": is_active,
|
|
41
|
+
}
|
|
42
|
+
filters = {k: v for k, v in filter_mapping.items() if v is not None}
|
|
43
|
+
|
|
44
|
+
users_list: list[User] = [user async for user in client.list_users(**filters)]
|
|
45
|
+
|
|
46
|
+
if is_organization_group_admin is not None:
|
|
47
|
+
org_admin_role_urls: set[str] = {
|
|
48
|
+
group.url async for group in client.list_user_roles() if group.name == "organization_group_admin"
|
|
49
|
+
}
|
|
50
|
+
if is_organization_group_admin:
|
|
51
|
+
users_list = [user for user in users_list if set(user.groups) & org_admin_role_urls]
|
|
52
|
+
else:
|
|
53
|
+
users_list = [user for user in users_list if not (set(user.groups) & org_admin_role_urls)]
|
|
54
|
+
|
|
55
|
+
return users_list
|
|
56
|
+
|
|
57
|
+
@mcp.tool(description="List all user roles (groups of permissions) in the organization.")
|
|
58
|
+
async def list_user_roles() -> list[Group]:
|
|
59
|
+
groups_list: list[Group] = [group async for group in client.list_user_roles()]
|
|
60
|
+
return groups_list
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Workspace 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.workspace import Workspace # noqa: TC002 - needed at runtime for FastMCP
|
|
9
|
+
|
|
10
|
+
from rossum_mcp.tools.base import build_resource_url, 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
|
+
|
|
19
|
+
def register_workspace_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None:
|
|
20
|
+
"""Register workspace-related tools with the FastMCP server."""
|
|
21
|
+
|
|
22
|
+
@mcp.tool(description="Retrieve workspace details.")
|
|
23
|
+
async def get_workspace(workspace_id: int) -> Workspace:
|
|
24
|
+
"""Retrieve workspace details."""
|
|
25
|
+
logger.debug(f"Retrieving workspace: workspace_id={workspace_id}")
|
|
26
|
+
workspace: Workspace = await client.retrieve_workspace(workspace_id)
|
|
27
|
+
return workspace
|
|
28
|
+
|
|
29
|
+
@mcp.tool(description="List all workspaces with optional filters.")
|
|
30
|
+
async def list_workspaces(organization_id: int | None = None, name: str | None = None) -> list[Workspace]:
|
|
31
|
+
"""List all workspaces with optional filters."""
|
|
32
|
+
logger.debug(f"Listing workspaces: organization_id={organization_id}, name={name}")
|
|
33
|
+
filters: dict[str, int | str] = {}
|
|
34
|
+
if organization_id is not None:
|
|
35
|
+
filters["organization"] = organization_id
|
|
36
|
+
if name is not None:
|
|
37
|
+
filters["name"] = name
|
|
38
|
+
|
|
39
|
+
return [
|
|
40
|
+
workspace
|
|
41
|
+
async for workspace in client.list_workspaces(**filters) # type: ignore[arg-type]
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
@mcp.tool(description="Create a new workspace.")
|
|
45
|
+
async def create_workspace(name: str, organization_id: int, metadata: dict | None = None) -> Workspace | dict:
|
|
46
|
+
"""Create a new workspace."""
|
|
47
|
+
if not is_read_write_mode():
|
|
48
|
+
return {"error": "create_workspace is not available in read-only mode"}
|
|
49
|
+
|
|
50
|
+
organization_url = build_resource_url("organizations", organization_id)
|
|
51
|
+
logger.info(
|
|
52
|
+
f"Creating workspace: name={name}, organization_id={organization_id}, "
|
|
53
|
+
f"organization_url={organization_url}, metadata={metadata}"
|
|
54
|
+
)
|
|
55
|
+
workspace_data: dict = {
|
|
56
|
+
"name": name,
|
|
57
|
+
"organization": organization_url,
|
|
58
|
+
}
|
|
59
|
+
if metadata is not None:
|
|
60
|
+
workspace_data["metadata"] = metadata
|
|
61
|
+
|
|
62
|
+
logger.debug(f"Workspace creation payload: {workspace_data}")
|
|
63
|
+
workspace: Workspace = await client.create_new_workspace(workspace_data)
|
|
64
|
+
logger.info(f"Successfully created workspace: id={workspace.id}, name={workspace.name}")
|
|
65
|
+
return workspace
|