rossum-mcp 0.3.4__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 ( # noqa: TC002 - needed at runtime for FastMCP
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 register_hook_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None: # noqa: C901
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
- hook: Hook = await client.retrieve_hook(hook_id)
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
- 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.
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
- 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
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
- 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
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
- 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
- ]
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
- 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
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
- 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."}
343
+ return await _create_hook_from_template(client, name, hook_template_id, queues, events, token_owner)