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.
@@ -0,0 +1,265 @@
1
+ """Hook tools for Rossum MCP Server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING, Annotated, Any, Literal
8
+
9
+ from rossum_api.models.hook import ( # noqa: TC002 - needed at runtime for FastMCP
10
+ Hook,
11
+ HookRunData,
12
+ HookType,
13
+ )
14
+
15
+ from rossum_mcp.tools.base import is_read_write_mode
16
+
17
+ if TYPE_CHECKING:
18
+ from fastmcp import FastMCP
19
+ from rossum_api import AsyncRossumAPIClient
20
+
21
+ type Timestamp = Annotated[str, "ISO 8601 timestamp (e.g., '2024-01-15T10:30:00Z')"]
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ @dataclass
27
+ class HookTemplate:
28
+ """Represents a hook template from Rossum Store.
29
+
30
+ Hook templates provide pre-built extension configurations that can be
31
+ used to quickly create hooks with standard functionality.
32
+ """
33
+
34
+ id: int
35
+ url: str
36
+ name: str
37
+ description: str
38
+ type: str
39
+ events: list[str]
40
+ config: dict[str, Any]
41
+ settings_schema: dict[str, Any] | None
42
+ guide: str | None
43
+ use_token_owner: bool
44
+
45
+
46
+ def register_hook_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None: # noqa: C901
47
+ """Register hook-related tools with the FastMCP server."""
48
+
49
+ @mcp.tool(
50
+ description="Retrieve a single hook by ID. Use list_hooks first to get all hooks for a queue - only use get_hook if you need additional details for a specific hook not returned by list_hooks. For Python-based function hooks, the source code is accessible via hook.config['code']."
51
+ )
52
+ async def get_hook(hook_id: int) -> Hook:
53
+ hook: Hook = await client.retrieve_hook(hook_id)
54
+ return hook
55
+
56
+ @mcp.tool(
57
+ description="List all hooks/extensions for a queue. ALWAYS use this first when you need information about hooks on a queue - it returns complete hook details including code, config, and settings in a single call. Only use get_hook afterward if you need details not present in the list response. For Python-based function hooks, the source code is accessible via hook.config['code']."
58
+ )
59
+ async def list_hooks(
60
+ queue_id: int | None = None, active: bool | None = None, first_n: int | None = None
61
+ ) -> list[Hook]:
62
+ filters: dict = {}
63
+ if queue_id is not None:
64
+ filters["queue"] = queue_id
65
+ if active is not None:
66
+ filters["active"] = active
67
+
68
+ if first_n is not None:
69
+ hooks_iter = client.list_hooks(**filters)
70
+ hooks_list: list[Hook] = []
71
+ n = 0
72
+ while n < first_n:
73
+ hooks_list.append(await anext(hooks_iter))
74
+ n += 1
75
+ else:
76
+ hooks_list = [hook async for hook in client.list_hooks(**filters)]
77
+
78
+ return hooks_list
79
+
80
+ # NOTE: We explicitly document token_owner restrictions in the tool description because
81
+ # Sonnet 4.5 respects tool descriptions more reliably than instructions in system prompts.
82
+ @mcp.tool(
83
+ description="Create a new hook. If token_owner is provided, organization_group_admin users CANNOT be used (API will reject)."
84
+ )
85
+ async def create_hook(
86
+ name: str,
87
+ type: HookType,
88
+ queues: list[str] | None = None,
89
+ events: list[str] | None = None,
90
+ config: dict | None = None,
91
+ settings: dict | None = None,
92
+ secret: str | None = None,
93
+ ) -> Hook | dict:
94
+ if not is_read_write_mode():
95
+ return {"error": "create_hook is not available in read-only mode"}
96
+
97
+ hook_data: dict[str, Any] = {"name": name, "type": type, "sideload": ["schemas"]}
98
+
99
+ if queues is not None:
100
+ hook_data["queues"] = queues
101
+ if events is not None:
102
+ hook_data["events"] = events
103
+ if config is None:
104
+ config = {}
105
+ if type == "function" and "source" in config:
106
+ config["function"] = config.pop("source")
107
+ if type == "function" and "runtime" not in config:
108
+ config["runtime"] = "python3.12"
109
+ if "timeout_s" in config and config["timeout_s"] > 60:
110
+ config["timeout_s"] = 60
111
+ hook_data["config"] = config
112
+ if settings is not None:
113
+ hook_data["settings"] = settings
114
+ if secret is not None:
115
+ hook_data["secret"] = secret
116
+
117
+ hook: Hook = await client.create_new_hook(hook_data)
118
+ return hook
119
+
120
+ @mcp.tool(
121
+ description="Update an existing hook. Use this to modify hook properties like name, queues, config, events, or settings. Only provide the fields you want to change - other fields will remain unchanged."
122
+ )
123
+ async def update_hook(
124
+ hook_id: int,
125
+ name: str | None = None,
126
+ queues: list[str] | None = None,
127
+ events: list[str] | None = None,
128
+ config: dict | None = None,
129
+ settings: dict | None = None,
130
+ active: bool | None = None,
131
+ ) -> Hook | dict:
132
+ if not is_read_write_mode():
133
+ return {"error": "update_hook is not available in read-only mode"}
134
+
135
+ logger.debug(f"Updating hook: hook_id={hook_id}")
136
+
137
+ # Fetch existing hook data first (PUT requires all fields)
138
+ existing_hook: Hook = await client.retrieve_hook(hook_id)
139
+ hook_data: dict[str, Any] = {
140
+ "name": existing_hook.name,
141
+ "queues": existing_hook.queues,
142
+ "events": list(existing_hook.events),
143
+ "config": dict(existing_hook.config) if existing_hook.config else {},
144
+ }
145
+
146
+ # Override with provided values
147
+ if name is not None:
148
+ hook_data["name"] = name
149
+ if queues is not None:
150
+ hook_data["queues"] = queues
151
+ if events is not None:
152
+ hook_data["events"] = events
153
+ if config is not None:
154
+ hook_data["config"] = config
155
+ if settings is not None:
156
+ hook_data["settings"] = settings
157
+ if active is not None:
158
+ hook_data["active"] = active
159
+
160
+ updated_hook: Hook = await client.update_part_hook(hook_id, hook_data)
161
+ return updated_hook
162
+
163
+ @mcp.tool(
164
+ description="List hook execution logs. Use this to debug hook executions, monitor performance, and troubleshoot errors. Logs are retained for 7 days. Returns at most 100 logs per call."
165
+ )
166
+ async def list_hook_logs(
167
+ hook_id: int | None = None,
168
+ queue_id: int | None = None,
169
+ annotation_id: int | None = None,
170
+ email_id: int | None = None,
171
+ log_level: Literal["INFO", "ERROR", "WARNING"] | None = None,
172
+ status: str | None = None,
173
+ status_code: int | None = None,
174
+ request_id: str | None = None,
175
+ timestamp_before: Timestamp | None = None,
176
+ timestamp_after: Timestamp | None = None,
177
+ start_before: Timestamp | None = None,
178
+ start_after: Timestamp | None = None,
179
+ end_before: Timestamp | None = None,
180
+ end_after: Timestamp | None = None,
181
+ search: str | None = None,
182
+ page_size: int | None = None,
183
+ ) -> list[HookRunData]:
184
+ filter_mapping: dict[str, Any] = {
185
+ "hook": hook_id,
186
+ "queue": queue_id,
187
+ "annotation": annotation_id,
188
+ "email": email_id,
189
+ "log_level": log_level,
190
+ "status": status,
191
+ "status_code": status_code,
192
+ "request_id": request_id,
193
+ "timestamp_before": timestamp_before,
194
+ "timestamp_after": timestamp_after,
195
+ "start_before": start_before,
196
+ "start_after": start_after,
197
+ "end_before": end_before,
198
+ "end_after": end_after,
199
+ "search": search,
200
+ "page_size": page_size,
201
+ }
202
+ filters = {k: v for k, v in filter_mapping.items() if v is not None}
203
+
204
+ # list_hook_run_data is available from ds-feat-hook-logs branch
205
+ return [
206
+ log
207
+ async for log in client.list_hook_run_data(**filters) # type: ignore[attr-defined]
208
+ ]
209
+
210
+ @mcp.tool(
211
+ description="List available hook templates from Rossum Store. Hook templates provide pre-built extension configurations (e.g., data validation, field mapping, notifications) that can be used to quickly create hooks instead of writing code from scratch. Use list_hook_templates first to find a suitable template, then use create_hook_from_template to create a hook based on that template."
212
+ )
213
+ async def list_hook_templates() -> list[HookTemplate]:
214
+ templates: list[HookTemplate] = []
215
+ async for item in client.request_paginated("hook_templates"):
216
+ url = item["url"]
217
+ templates.append(
218
+ HookTemplate(
219
+ id=int(url.split("/")[-1]),
220
+ url=url,
221
+ name=item["name"],
222
+ description=item.get("description", ""),
223
+ type=item["type"],
224
+ events=[],
225
+ config={},
226
+ settings_schema=item.get("settings_schema"),
227
+ guide="<truncated>",
228
+ use_token_owner=item.get("use_token_owner", False),
229
+ )
230
+ )
231
+ return templates
232
+
233
+ # NOTE: We explicitly document token_owner restrictions in the tool description because
234
+ # Sonnet 4.5 respects tool descriptions more reliably than instructions in system prompts.
235
+ @mcp.tool(
236
+ description="Create a hook from a Rossum Store template. Uses pre-built configurations from the Rossum Store. The 'events' parameter is optional and can override template defaults. If the template has 'use_token_owner=True', a valid 'token_owner' user URL is required - use list_users to find one. CRITICAL RESTRICTION: organization_group_admin users are FORBIDDEN as token_owner - the API returns HTTP 400 error."
237
+ )
238
+ async def create_hook_from_template(
239
+ name: str,
240
+ hook_template_id: int,
241
+ queues: list[str],
242
+ events: list[str] | None = None,
243
+ token_owner: str | None = None,
244
+ ) -> Hook | dict:
245
+ if not is_read_write_mode():
246
+ return {"error": "create_hook_from_template is not available in read-only mode"}
247
+
248
+ logger.debug(f"Creating hook from template: name={name}, template_id={hook_template_id}")
249
+
250
+ # Build the hook template URL and fetch template to get its config
251
+ hook_template_url = f"{client._http_client.base_url.rstrip('/')}/hook_templates/{hook_template_id}"
252
+
253
+ hook_data: dict[str, Any] = {"name": name, "hook_template": hook_template_url, "queues": queues}
254
+ if events is not None:
255
+ hook_data["events"] = events
256
+ if token_owner is not None:
257
+ hook_data["token_owner"] = token_owner
258
+
259
+ result = await client._http_client.request_json("POST", "hooks/create", json=hook_data)
260
+
261
+ # Return the created hook
262
+ if hook_id := result.get("id"):
263
+ hook: Hook = await client.retrieve_hook(hook_id)
264
+ return hook
265
+ return {"error": "Hook wasn't likely created. Hook ID not available."}
@@ -0,0 +1,133 @@
1
+ """Queue tools for Rossum MCP Server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from typing import TYPE_CHECKING
8
+
9
+ from rossum_api import APIClientError
10
+ from rossum_api.domain_logic.resources import Resource
11
+ 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
15
+
16
+ from rossum_mcp.tools.base import build_resource_url, is_read_write_mode
17
+
18
+ if TYPE_CHECKING:
19
+ from fastmcp import FastMCP
20
+ from rossum_api import AsyncRossumAPIClient
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def register_queue_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None: # noqa: C901
26
+ """Register queue-related tools with the FastMCP server."""
27
+
28
+ @mcp.tool(description="Retrieve queue details.")
29
+ 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
34
+
35
+ @mcp.tool(description="Retrieve queue schema.")
36
+ 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
44
+
45
+ @mcp.tool(description="Retrieve queue engine. Returns None if no engine assigned.")
46
+ 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
74
+
75
+ @mcp.tool(description="Create a queue.")
76
+ async def create_queue(
77
+ name: str,
78
+ workspace_id: int,
79
+ schema_id: int,
80
+ engine_id: int | None = None,
81
+ inbox_id: int | None = None,
82
+ connector_id: int | None = None,
83
+ locale: str = "en_GB",
84
+ automation_enabled: bool = False,
85
+ automation_level: str = "never",
86
+ training_enabled: bool = True,
87
+ splitting_screen_feature_flag: bool = False,
88
+ ) -> 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}"
95
+ )
96
+
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
+ @mcp.tool(description="Update queue settings.")
125
+ 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
@@ -0,0 +1,56 @@
1
+ """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.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
+ )
13
+
14
+ if TYPE_CHECKING:
15
+ from fastmcp import FastMCP
16
+ from rossum_api import AsyncRossumAPIClient
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def register_relation_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None:
22
+ """Register relation-related tools with the FastMCP server."""
23
+
24
+ @mcp.tool(description="Retrieve relation details.")
25
+ async def get_relation(relation_id: int) -> Relation:
26
+ """Retrieve relation details."""
27
+ logger.debug(f"Retrieving relation: relation_id={relation_id}")
28
+ 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
31
+
32
+ @mcp.tool(
33
+ description="List all relations with optional filters. Relations introduce common relations between annotations (edit, attachment, duplicate)."
34
+ )
35
+ async def list_relations(
36
+ id: int | None = None,
37
+ type: RelationType | None = None,
38
+ parent: int | None = None,
39
+ key: str | None = None,
40
+ annotation: int | None = None,
41
+ ) -> list[Relation]:
42
+ """List all relations with optional filters."""
43
+ logger.debug(f"Listing relations: id={id}, type={type}, parent={parent}, key={key}, annotation={annotation}")
44
+ filters: dict[str, int | str] = {}
45
+ if id is not None:
46
+ filters["id"] = id
47
+ if type is not None:
48
+ filters["type"] = type
49
+ if parent is not None:
50
+ filters["parent"] = parent
51
+ if key is not None:
52
+ filters["key"] = key
53
+ if annotation is not None:
54
+ filters["annotation"] = annotation
55
+
56
+ return [relation async for relation in client.list_relations(**filters)] # type: ignore[arg-type]
@@ -0,0 +1,42 @@
1
+ """Rule 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.rule import Rule # noqa: TC002 - needed at runtime for FastMCP
9
+
10
+ if TYPE_CHECKING:
11
+ from fastmcp import FastMCP
12
+ from rossum_api import AsyncRossumAPIClient
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def register_rule_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None:
18
+ """Register rule-related tools with the FastMCP server."""
19
+
20
+ @mcp.tool(description="Retrieve rule details.")
21
+ 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
26
+
27
+ @mcp.tool(description="List all rules.")
28
+ async def list_rules(
29
+ schema_id: int | None = None, organization_id: int | None = None, enabled: bool | None = None
30
+ ) -> 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