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 +1 -1
- rossum_mcp/server.py +2 -0
- rossum_mcp/tools/__init__.py +2 -0
- rossum_mcp/tools/annotations.py +123 -86
- rossum_mcp/tools/base.py +22 -0
- rossum_mcp/tools/document_relations.py +1 -3
- rossum_mcp/tools/email_templates.py +131 -0
- rossum_mcp/tools/engines.py +106 -77
- rossum_mcp/tools/hooks.py +228 -150
- rossum_mcp/tools/queues.py +251 -85
- rossum_mcp/tools/relations.py +3 -7
- rossum_mcp/tools/rules.py +28 -17
- rossum_mcp/tools/schemas.py +501 -104
- rossum_mcp/tools/users.py +51 -27
- rossum_mcp/tools/workspaces.py +50 -37
- {rossum_mcp-0.3.4.dist-info → rossum_mcp-0.3.5.dist-info}/METADATA +255 -5
- rossum_mcp-0.3.5.dist-info/RECORD +22 -0
- rossum_mcp-0.3.4.dist-info/RECORD +0 -21
- {rossum_mcp-0.3.4.dist-info → rossum_mcp-0.3.5.dist-info}/WHEEL +0 -0
- {rossum_mcp-0.3.4.dist-info → rossum_mcp-0.3.5.dist-info}/entry_points.txt +0 -0
- {rossum_mcp-0.3.4.dist-info → rossum_mcp-0.3.5.dist-info}/top_level.txt +0 -0
rossum_mcp/tools/engines.py
CHANGED
|
@@ -3,14 +3,10 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
-
from typing import TYPE_CHECKING, Literal
|
|
6
|
+
from typing import TYPE_CHECKING, Literal, cast
|
|
7
7
|
|
|
8
8
|
from rossum_api.domain_logic.resources import Resource
|
|
9
|
-
from rossum_api.models.engine import
|
|
10
|
-
Engine, # noqa: TC002 - needed at runtime for FastMCP
|
|
11
|
-
EngineField, # noqa: TC002 - needed at runtime for FastMCP
|
|
12
|
-
EngineFieldType, # noqa: TC002 - needed at runtime for FastMCP
|
|
13
|
-
)
|
|
9
|
+
from rossum_api.models.engine import Engine, EngineField, EngineFieldType
|
|
14
10
|
|
|
15
11
|
from rossum_mcp.tools.base import build_resource_url, is_read_write_mode
|
|
16
12
|
|
|
@@ -23,62 +19,124 @@ if TYPE_CHECKING:
|
|
|
23
19
|
logger = logging.getLogger(__name__)
|
|
24
20
|
|
|
25
21
|
|
|
26
|
-
def
|
|
22
|
+
async def _get_engine(client: AsyncRossumAPIClient, engine_id: int) -> Engine:
|
|
23
|
+
logger.debug(f"Retrieving engine: engine_id={engine_id}")
|
|
24
|
+
engine: Engine = await client.retrieve_engine(engine_id)
|
|
25
|
+
return engine
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def _list_engines(
|
|
29
|
+
client: AsyncRossumAPIClient,
|
|
30
|
+
id: int | None = None,
|
|
31
|
+
engine_type: EngineType | None = None,
|
|
32
|
+
agenda_id: str | None = None,
|
|
33
|
+
) -> list[Engine]:
|
|
34
|
+
logger.debug(f"Listing engines: id={id}, type={engine_type}, agenda_id={agenda_id}")
|
|
35
|
+
filters: dict[str, int | str] = {}
|
|
36
|
+
if id is not None:
|
|
37
|
+
filters["id"] = id
|
|
38
|
+
if engine_type is not None:
|
|
39
|
+
filters["type"] = engine_type
|
|
40
|
+
if agenda_id is not None:
|
|
41
|
+
filters["agenda_id"] = agenda_id
|
|
42
|
+
return [engine async for engine in client.list_engines(**filters)] # type: ignore[arg-type]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def _update_engine(client: AsyncRossumAPIClient, engine_id: int, engine_data: dict) -> Engine | dict:
|
|
46
|
+
if not is_read_write_mode():
|
|
47
|
+
return {"error": "update_engine is not available in read-only mode"}
|
|
48
|
+
|
|
49
|
+
logger.debug(f"Updating engine: engine_id={engine_id}, data={engine_data}")
|
|
50
|
+
updated_engine_data = await client._http_client.update(Resource.Engine, engine_id, engine_data)
|
|
51
|
+
return cast("Engine", client._deserializer(Resource.Engine, updated_engine_data))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def _create_engine(
|
|
55
|
+
client: AsyncRossumAPIClient, name: str, organization_id: int, engine_type: EngineType
|
|
56
|
+
) -> Engine | dict:
|
|
57
|
+
if not is_read_write_mode():
|
|
58
|
+
return {"error": "create_engine is not available in read-only mode"}
|
|
59
|
+
|
|
60
|
+
if engine_type not in ("extractor", "splitter"):
|
|
61
|
+
raise ValueError(f"Invalid engine_type '{engine_type}'. Must be 'extractor' or 'splitter'")
|
|
62
|
+
|
|
63
|
+
logger.debug(f"Creating engine: name={name}, organization_id={organization_id}, type={engine_type}")
|
|
64
|
+
engine_data = {
|
|
65
|
+
"name": name,
|
|
66
|
+
"organization": build_resource_url("organizations", organization_id),
|
|
67
|
+
"type": engine_type,
|
|
68
|
+
}
|
|
69
|
+
engine_response = await client._http_client.create(Resource.Engine, engine_data)
|
|
70
|
+
return cast("Engine", client._deserializer(Resource.Engine, engine_response))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def _create_engine_field(
|
|
74
|
+
client: AsyncRossumAPIClient,
|
|
75
|
+
engine_id: int,
|
|
76
|
+
name: str,
|
|
77
|
+
label: str,
|
|
78
|
+
field_type: EngineFieldType,
|
|
79
|
+
schema_ids: list[int],
|
|
80
|
+
tabular: bool = False,
|
|
81
|
+
multiline: str = "false",
|
|
82
|
+
subtype: str | None = None,
|
|
83
|
+
pre_trained_field_id: str | None = None,
|
|
84
|
+
) -> EngineField | dict:
|
|
85
|
+
if not is_read_write_mode():
|
|
86
|
+
return {"error": "create_engine_field is not available in read-only mode"}
|
|
87
|
+
|
|
88
|
+
valid_types = ("string", "number", "date", "enum")
|
|
89
|
+
if field_type not in valid_types:
|
|
90
|
+
raise ValueError(f"Invalid field_type '{field_type}'. Must be one of: {', '.join(valid_types)}")
|
|
91
|
+
if not schema_ids:
|
|
92
|
+
raise ValueError("schema_ids cannot be empty - engine field must be linked to at least one schema")
|
|
93
|
+
|
|
94
|
+
logger.debug(f"Creating engine field: engine_id={engine_id}, name={name}, type={field_type}, schemas={schema_ids}")
|
|
95
|
+
engine_field_data = {
|
|
96
|
+
"engine": build_resource_url("engines", engine_id),
|
|
97
|
+
"name": name,
|
|
98
|
+
"label": label,
|
|
99
|
+
"type": field_type,
|
|
100
|
+
"tabular": tabular,
|
|
101
|
+
"multiline": multiline,
|
|
102
|
+
"schemas": [build_resource_url("schemas", schema_id) for schema_id in schema_ids],
|
|
103
|
+
}
|
|
104
|
+
if subtype is not None:
|
|
105
|
+
engine_field_data["subtype"] = subtype
|
|
106
|
+
if pre_trained_field_id is not None:
|
|
107
|
+
engine_field_data["pre_trained_field_id"] = pre_trained_field_id
|
|
108
|
+
|
|
109
|
+
engine_field_response = await client._http_client.create(Resource.EngineField, engine_field_data)
|
|
110
|
+
return cast("EngineField", client._deserializer(Resource.EngineField, engine_field_response))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
async def _get_engine_fields(client: AsyncRossumAPIClient, engine_id: int | None = None) -> list[EngineField]:
|
|
114
|
+
logger.debug(f"Retrieving engine fields: engine_id={engine_id}")
|
|
115
|
+
return [engine_field async for engine_field in client.retrieve_engine_fields(engine_id=engine_id)]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def register_engine_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None:
|
|
27
119
|
"""Register engine-related tools with the FastMCP server."""
|
|
28
120
|
|
|
29
121
|
@mcp.tool(description="Retrieve a single engine by ID.")
|
|
30
122
|
async def get_engine(engine_id: int) -> Engine:
|
|
31
|
-
|
|
32
|
-
logger.debug(f"Retrieving engine: engine_id={engine_id}")
|
|
33
|
-
engine: Engine = await client.retrieve_engine(engine_id)
|
|
34
|
-
return engine
|
|
123
|
+
return await _get_engine(client, engine_id)
|
|
35
124
|
|
|
36
125
|
@mcp.tool(description="List all engines with optional filters.")
|
|
37
126
|
async def list_engines(
|
|
38
127
|
id: int | None = None, engine_type: EngineType | None = None, agenda_id: str | None = None
|
|
39
128
|
) -> list[Engine]:
|
|
40
|
-
|
|
41
|
-
logger.debug(f"Listing engines: id={id}, type={engine_type}, agenda_id={agenda_id}")
|
|
42
|
-
filters: dict[str, int | str] = {}
|
|
43
|
-
if id is not None:
|
|
44
|
-
filters["id"] = id
|
|
45
|
-
if engine_type is not None:
|
|
46
|
-
filters["type"] = engine_type
|
|
47
|
-
if agenda_id is not None:
|
|
48
|
-
filters["agenda_id"] = agenda_id
|
|
49
|
-
return [engine async for engine in client.list_engines(**filters)] # type: ignore[arg-type]
|
|
129
|
+
return await _list_engines(client, id, engine_type, agenda_id)
|
|
50
130
|
|
|
51
131
|
@mcp.tool(description="Update engine settings.")
|
|
52
132
|
async def update_engine(engine_id: int, engine_data: dict) -> Engine | dict:
|
|
53
|
-
|
|
54
|
-
if not is_read_write_mode():
|
|
55
|
-
return {"error": "update_engine is not available in read-only mode"}
|
|
56
|
-
|
|
57
|
-
logger.debug(f"Updating engine: engine_id={engine_id}, data={engine_data}")
|
|
58
|
-
updated_engine_data = await client._http_client.update(Resource.Engine, engine_id, engine_data)
|
|
59
|
-
updated_engine: Engine = client._deserializer(Resource.Engine, updated_engine_data)
|
|
60
|
-
return updated_engine
|
|
133
|
+
return await _update_engine(client, engine_id, engine_data)
|
|
61
134
|
|
|
62
135
|
@mcp.tool(
|
|
63
136
|
description="Create a new engine. IMPORTANT: When creating a new engine, check the schema to be used and create contained Engine fields immediately!"
|
|
64
137
|
)
|
|
65
138
|
async def create_engine(name: str, organization_id: int, engine_type: EngineType) -> Engine | dict:
|
|
66
|
-
|
|
67
|
-
if not is_read_write_mode():
|
|
68
|
-
return {"error": "create_engine is not available in read-only mode"}
|
|
69
|
-
|
|
70
|
-
if engine_type not in ("extractor", "splitter"):
|
|
71
|
-
raise ValueError(f"Invalid engine_type '{engine_type}'. Must be 'extractor' or 'splitter'")
|
|
72
|
-
|
|
73
|
-
logger.debug(f"Creating engine: name={name}, organization_id={organization_id}, type={engine_type}")
|
|
74
|
-
engine_data = {
|
|
75
|
-
"name": name,
|
|
76
|
-
"organization": build_resource_url("organizations", organization_id),
|
|
77
|
-
"type": engine_type,
|
|
78
|
-
}
|
|
79
|
-
engine_response = await client._http_client.create(Resource.Engine, engine_data)
|
|
80
|
-
engine: Engine = client._deserializer(Resource.Engine, engine_response)
|
|
81
|
-
return engine
|
|
139
|
+
return await _create_engine(client, name, organization_id, engine_type)
|
|
82
140
|
|
|
83
141
|
@mcp.tool(description="Create engine field for each schema field. Must be called when creating engine + schema.")
|
|
84
142
|
async def create_engine_field(
|
|
@@ -92,39 +150,10 @@ def register_engine_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None:
|
|
|
92
150
|
subtype: str | None = None,
|
|
93
151
|
pre_trained_field_id: str | None = None,
|
|
94
152
|
) -> EngineField | dict:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return {"error": "create_engine_field is not available in read-only mode"}
|
|
98
|
-
|
|
99
|
-
valid_types = ("string", "number", "date", "enum")
|
|
100
|
-
if field_type not in valid_types:
|
|
101
|
-
raise ValueError(f"Invalid field_type '{field_type}'. Must be one of: {', '.join(valid_types)}")
|
|
102
|
-
if not schema_ids:
|
|
103
|
-
raise ValueError("schema_ids cannot be empty - engine field must be linked to at least one schema")
|
|
104
|
-
|
|
105
|
-
logger.debug(
|
|
106
|
-
f"Creating engine field: engine_id={engine_id}, name={name}, type={field_type}, schemas={schema_ids}"
|
|
153
|
+
return await _create_engine_field(
|
|
154
|
+
client, engine_id, name, label, field_type, schema_ids, tabular, multiline, subtype, pre_trained_field_id
|
|
107
155
|
)
|
|
108
|
-
engine_field_data = {
|
|
109
|
-
"engine": build_resource_url("engines", engine_id),
|
|
110
|
-
"name": name,
|
|
111
|
-
"label": label,
|
|
112
|
-
"type": field_type,
|
|
113
|
-
"tabular": tabular,
|
|
114
|
-
"multiline": multiline,
|
|
115
|
-
"schemas": [build_resource_url("schemas", schema_id) for schema_id in schema_ids],
|
|
116
|
-
}
|
|
117
|
-
if subtype is not None:
|
|
118
|
-
engine_field_data["subtype"] = subtype
|
|
119
|
-
if pre_trained_field_id is not None:
|
|
120
|
-
engine_field_data["pre_trained_field_id"] = pre_trained_field_id
|
|
121
|
-
|
|
122
|
-
engine_field_response = await client._http_client.create(Resource.EngineField, engine_field_data)
|
|
123
|
-
engine_field: EngineField = client._deserializer(Resource.EngineField, engine_field_response)
|
|
124
|
-
return engine_field
|
|
125
156
|
|
|
126
157
|
@mcp.tool(description="Retrieve engine fields for a specific engine or all engine fields.")
|
|
127
158
|
async def get_engine_fields(engine_id: int | None = None) -> list[EngineField]:
|
|
128
|
-
|
|
129
|
-
logger.debug(f"Retrieving engine fields: engine_id={engine_id}")
|
|
130
|
-
return [engine_field async for engine_field in client.retrieve_engine_fields(engine_id=engine_id)]
|
|
159
|
+
return await _get_engine_fields(client, engine_id)
|
rossum_mcp/tools/hooks.py
CHANGED
|
@@ -6,13 +6,9 @@ import logging
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from typing import TYPE_CHECKING, Annotated, Any, Literal
|
|
8
8
|
|
|
9
|
-
from rossum_api.models.hook import
|
|
10
|
-
Hook,
|
|
11
|
-
HookRunData,
|
|
12
|
-
HookType,
|
|
13
|
-
)
|
|
9
|
+
from rossum_api.models.hook import Hook, HookRunData, HookType
|
|
14
10
|
|
|
15
|
-
from rossum_mcp.tools.base import is_read_write_mode
|
|
11
|
+
from rossum_mcp.tools.base import TRUNCATED_MARKER, is_read_write_mode
|
|
16
12
|
|
|
17
13
|
if TYPE_CHECKING:
|
|
18
14
|
from fastmcp import FastMCP
|
|
@@ -43,15 +39,213 @@ class HookTemplate:
|
|
|
43
39
|
use_token_owner: bool
|
|
44
40
|
|
|
45
41
|
|
|
46
|
-
def
|
|
42
|
+
async def _get_hook(client: AsyncRossumAPIClient, hook_id: int) -> Hook:
|
|
43
|
+
hook: Hook = await client.retrieve_hook(hook_id)
|
|
44
|
+
return hook
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def _list_hooks(
|
|
48
|
+
client: AsyncRossumAPIClient,
|
|
49
|
+
queue_id: int | None = None,
|
|
50
|
+
active: bool | None = None,
|
|
51
|
+
first_n: int | None = None,
|
|
52
|
+
) -> list[Hook]:
|
|
53
|
+
filters: dict = {}
|
|
54
|
+
if queue_id is not None:
|
|
55
|
+
filters["queue"] = queue_id
|
|
56
|
+
if active is not None:
|
|
57
|
+
filters["active"] = active
|
|
58
|
+
|
|
59
|
+
if first_n is not None:
|
|
60
|
+
hooks_iter = client.list_hooks(**filters)
|
|
61
|
+
hooks_list: list[Hook] = []
|
|
62
|
+
n = 0
|
|
63
|
+
while n < first_n:
|
|
64
|
+
hooks_list.append(await anext(hooks_iter))
|
|
65
|
+
n += 1
|
|
66
|
+
else:
|
|
67
|
+
hooks_list = [hook async for hook in client.list_hooks(**filters)]
|
|
68
|
+
|
|
69
|
+
return hooks_list
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def _create_hook(
|
|
73
|
+
client: AsyncRossumAPIClient,
|
|
74
|
+
name: str,
|
|
75
|
+
type: HookType,
|
|
76
|
+
queues: list[str] | None = None,
|
|
77
|
+
events: list[str] | None = None,
|
|
78
|
+
config: dict | None = None,
|
|
79
|
+
settings: dict | None = None,
|
|
80
|
+
secret: str | None = None,
|
|
81
|
+
) -> Hook | dict:
|
|
82
|
+
if not is_read_write_mode():
|
|
83
|
+
return {"error": "create_hook is not available in read-only mode"}
|
|
84
|
+
|
|
85
|
+
hook_data: dict[str, Any] = {"name": name, "type": type, "sideload": ["schemas"]}
|
|
86
|
+
|
|
87
|
+
if queues is not None:
|
|
88
|
+
hook_data["queues"] = queues
|
|
89
|
+
if events is not None:
|
|
90
|
+
hook_data["events"] = events
|
|
91
|
+
if config is None:
|
|
92
|
+
config = {}
|
|
93
|
+
if type == "function" and "source" in config:
|
|
94
|
+
config["function"] = config.pop("source")
|
|
95
|
+
if type == "function" and "runtime" not in config:
|
|
96
|
+
config["runtime"] = "python3.12"
|
|
97
|
+
if "timeout_s" in config and config["timeout_s"] > 60:
|
|
98
|
+
config["timeout_s"] = 60
|
|
99
|
+
hook_data["config"] = config
|
|
100
|
+
if settings is not None:
|
|
101
|
+
hook_data["settings"] = settings
|
|
102
|
+
if secret is not None:
|
|
103
|
+
hook_data["secret"] = secret
|
|
104
|
+
|
|
105
|
+
hook: Hook = await client.create_new_hook(hook_data)
|
|
106
|
+
return hook
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def _update_hook(
|
|
110
|
+
client: AsyncRossumAPIClient,
|
|
111
|
+
hook_id: int,
|
|
112
|
+
name: str | None = None,
|
|
113
|
+
queues: list[str] | None = None,
|
|
114
|
+
events: list[str] | None = None,
|
|
115
|
+
config: dict | None = None,
|
|
116
|
+
settings: dict | None = None,
|
|
117
|
+
active: bool | None = None,
|
|
118
|
+
) -> Hook | dict:
|
|
119
|
+
if not is_read_write_mode():
|
|
120
|
+
return {"error": "update_hook is not available in read-only mode"}
|
|
121
|
+
|
|
122
|
+
logger.debug(f"Updating hook: hook_id={hook_id}")
|
|
123
|
+
|
|
124
|
+
existing_hook: Hook = await client.retrieve_hook(hook_id)
|
|
125
|
+
hook_data: dict[str, Any] = {
|
|
126
|
+
"name": existing_hook.name,
|
|
127
|
+
"queues": existing_hook.queues,
|
|
128
|
+
"events": list(existing_hook.events),
|
|
129
|
+
"config": dict(existing_hook.config) if existing_hook.config else {},
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if name is not None:
|
|
133
|
+
hook_data["name"] = name
|
|
134
|
+
if queues is not None:
|
|
135
|
+
hook_data["queues"] = queues
|
|
136
|
+
if events is not None:
|
|
137
|
+
hook_data["events"] = events
|
|
138
|
+
if config is not None:
|
|
139
|
+
hook_data["config"] = config
|
|
140
|
+
if settings is not None:
|
|
141
|
+
hook_data["settings"] = settings
|
|
142
|
+
if active is not None:
|
|
143
|
+
hook_data["active"] = active
|
|
144
|
+
|
|
145
|
+
updated_hook: Hook = await client.update_part_hook(hook_id, hook_data)
|
|
146
|
+
return updated_hook
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def _list_hook_logs(
|
|
150
|
+
client: AsyncRossumAPIClient,
|
|
151
|
+
hook_id: int | None = None,
|
|
152
|
+
queue_id: int | None = None,
|
|
153
|
+
annotation_id: int | None = None,
|
|
154
|
+
email_id: int | None = None,
|
|
155
|
+
log_level: Literal["INFO", "ERROR", "WARNING"] | None = None,
|
|
156
|
+
status: str | None = None,
|
|
157
|
+
status_code: int | None = None,
|
|
158
|
+
request_id: str | None = None,
|
|
159
|
+
timestamp_before: Timestamp | None = None,
|
|
160
|
+
timestamp_after: Timestamp | None = None,
|
|
161
|
+
start_before: Timestamp | None = None,
|
|
162
|
+
start_after: Timestamp | None = None,
|
|
163
|
+
end_before: Timestamp | None = None,
|
|
164
|
+
end_after: Timestamp | None = None,
|
|
165
|
+
search: str | None = None,
|
|
166
|
+
page_size: int | None = None,
|
|
167
|
+
) -> list[HookRunData]:
|
|
168
|
+
filter_mapping: dict[str, Any] = {
|
|
169
|
+
"hook": hook_id,
|
|
170
|
+
"queue": queue_id,
|
|
171
|
+
"annotation": annotation_id,
|
|
172
|
+
"email": email_id,
|
|
173
|
+
"log_level": log_level,
|
|
174
|
+
"status": status,
|
|
175
|
+
"status_code": status_code,
|
|
176
|
+
"request_id": request_id,
|
|
177
|
+
"timestamp_before": timestamp_before,
|
|
178
|
+
"timestamp_after": timestamp_after,
|
|
179
|
+
"start_before": start_before,
|
|
180
|
+
"start_after": start_after,
|
|
181
|
+
"end_before": end_before,
|
|
182
|
+
"end_after": end_after,
|
|
183
|
+
"search": search,
|
|
184
|
+
"page_size": page_size,
|
|
185
|
+
}
|
|
186
|
+
filters = {k: v for k, v in filter_mapping.items() if v is not None}
|
|
187
|
+
|
|
188
|
+
return [log async for log in client.list_hook_run_data(**filters)]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
async def _list_hook_templates(client: AsyncRossumAPIClient) -> list[HookTemplate]:
|
|
192
|
+
templates: list[HookTemplate] = []
|
|
193
|
+
async for item in client.request_paginated("hook_templates"):
|
|
194
|
+
url = item["url"]
|
|
195
|
+
templates.append(
|
|
196
|
+
HookTemplate(
|
|
197
|
+
id=int(url.split("/")[-1]),
|
|
198
|
+
url=url,
|
|
199
|
+
name=item["name"],
|
|
200
|
+
description=item.get("description", ""),
|
|
201
|
+
type=item["type"],
|
|
202
|
+
events=[],
|
|
203
|
+
config={},
|
|
204
|
+
settings_schema=item.get("settings_schema"),
|
|
205
|
+
guide=TRUNCATED_MARKER,
|
|
206
|
+
use_token_owner=item.get("use_token_owner", False),
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
return templates
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
async def _create_hook_from_template(
|
|
213
|
+
client: AsyncRossumAPIClient,
|
|
214
|
+
name: str,
|
|
215
|
+
hook_template_id: int,
|
|
216
|
+
queues: list[str],
|
|
217
|
+
events: list[str] | None = None,
|
|
218
|
+
token_owner: str | None = None,
|
|
219
|
+
) -> Hook | dict:
|
|
220
|
+
if not is_read_write_mode():
|
|
221
|
+
return {"error": "create_hook_from_template is not available in read-only mode"}
|
|
222
|
+
|
|
223
|
+
logger.debug(f"Creating hook from template: name={name}, template_id={hook_template_id}")
|
|
224
|
+
|
|
225
|
+
hook_template_url = f"{client._http_client.base_url.rstrip('/')}/hook_templates/{hook_template_id}"
|
|
226
|
+
|
|
227
|
+
hook_data: dict[str, Any] = {"name": name, "hook_template": hook_template_url, "queues": queues}
|
|
228
|
+
if events is not None:
|
|
229
|
+
hook_data["events"] = events
|
|
230
|
+
if token_owner is not None:
|
|
231
|
+
hook_data["token_owner"] = token_owner
|
|
232
|
+
|
|
233
|
+
result = await client._http_client.request_json("POST", "hooks/create", json=hook_data)
|
|
234
|
+
|
|
235
|
+
if hook_id := result.get("id"):
|
|
236
|
+
hook: Hook = await client.retrieve_hook(hook_id)
|
|
237
|
+
return hook
|
|
238
|
+
return {"error": "Hook wasn't likely created. Hook ID not available."}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def register_hook_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None:
|
|
47
242
|
"""Register hook-related tools with the FastMCP server."""
|
|
48
243
|
|
|
49
244
|
@mcp.tool(
|
|
50
245
|
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
246
|
)
|
|
52
247
|
async def get_hook(hook_id: int) -> Hook:
|
|
53
|
-
|
|
54
|
-
return hook
|
|
248
|
+
return await _get_hook(client, hook_id)
|
|
55
249
|
|
|
56
250
|
@mcp.tool(
|
|
57
251
|
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']."
|
|
@@ -59,26 +253,8 @@ def register_hook_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None: #
|
|
|
59
253
|
async def list_hooks(
|
|
60
254
|
queue_id: int | None = None, active: bool | None = None, first_n: int | None = None
|
|
61
255
|
) -> list[Hook]:
|
|
62
|
-
|
|
63
|
-
|
|
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.
|
|
256
|
+
return await _list_hooks(client, queue_id, active, first_n)
|
|
257
|
+
|
|
82
258
|
@mcp.tool(
|
|
83
259
|
description="Create a new hook. If token_owner is provided, organization_group_admin users CANNOT be used (API will reject)."
|
|
84
260
|
)
|
|
@@ -91,31 +267,7 @@ def register_hook_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None: #
|
|
|
91
267
|
settings: dict | None = None,
|
|
92
268
|
secret: str | None = None,
|
|
93
269
|
) -> Hook | dict:
|
|
94
|
-
|
|
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
|
|
270
|
+
return await _create_hook(client, name, type, queues, events, config, settings, secret)
|
|
119
271
|
|
|
120
272
|
@mcp.tool(
|
|
121
273
|
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."
|
|
@@ -129,36 +281,7 @@ def register_hook_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None: #
|
|
|
129
281
|
settings: dict | None = None,
|
|
130
282
|
active: bool | None = None,
|
|
131
283
|
) -> Hook | dict:
|
|
132
|
-
|
|
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
|
|
284
|
+
return await _update_hook(client, hook_id, name, queues, events, config, settings, active)
|
|
162
285
|
|
|
163
286
|
@mcp.tool(
|
|
164
287
|
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."
|
|
@@ -181,57 +304,32 @@ def register_hook_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None: #
|
|
|
181
304
|
search: str | None = None,
|
|
182
305
|
page_size: int | None = None,
|
|
183
306
|
) -> list[HookRunData]:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
]
|
|
307
|
+
return await _list_hook_logs(
|
|
308
|
+
client,
|
|
309
|
+
hook_id,
|
|
310
|
+
queue_id,
|
|
311
|
+
annotation_id,
|
|
312
|
+
email_id,
|
|
313
|
+
log_level,
|
|
314
|
+
status,
|
|
315
|
+
status_code,
|
|
316
|
+
request_id,
|
|
317
|
+
timestamp_before,
|
|
318
|
+
timestamp_after,
|
|
319
|
+
start_before,
|
|
320
|
+
start_after,
|
|
321
|
+
end_before,
|
|
322
|
+
end_after,
|
|
323
|
+
search,
|
|
324
|
+
page_size,
|
|
325
|
+
)
|
|
209
326
|
|
|
210
327
|
@mcp.tool(
|
|
211
328
|
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
329
|
)
|
|
213
330
|
async def list_hook_templates() -> list[HookTemplate]:
|
|
214
|
-
|
|
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
|
|
331
|
+
return await _list_hook_templates(client)
|
|
232
332
|
|
|
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
333
|
@mcp.tool(
|
|
236
334
|
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
335
|
)
|
|
@@ -242,24 +340,4 @@ def register_hook_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None: #
|
|
|
242
340
|
events: list[str] | None = None,
|
|
243
341
|
token_owner: str | None = None,
|
|
244
342
|
) -> Hook | dict:
|
|
245
|
-
|
|
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."}
|
|
343
|
+
return await _create_hook_from_template(client, name, hook_template_id, queues, events, token_owner)
|