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/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.3.4"
3
+ __version__ = "0.4.0"
rossum_mcp/server.py CHANGED
@@ -17,7 +17,9 @@ from rossum_api.dtos import Token
17
17
  from rossum_mcp.logging_config import setup_logging
18
18
  from rossum_mcp.tools import (
19
19
  register_annotation_tools,
20
+ register_discovery_tools,
20
21
  register_document_relation_tools,
22
+ register_email_template_tools,
21
23
  register_engine_tools,
22
24
  register_hook_tools,
23
25
  register_queue_tools,
@@ -44,11 +46,13 @@ logger.info(f"Rossum MCP Server starting in {MODE} mode")
44
46
  mcp = FastMCP("rossum-mcp-server")
45
47
  client = AsyncRossumAPIClient(base_url=BASE_URL, credentials=Token(token=API_TOKEN))
46
48
 
49
+ register_discovery_tools(mcp)
47
50
  register_annotation_tools(mcp, client)
48
51
  register_queue_tools(mcp, client)
49
52
  register_schema_tools(mcp, client)
50
53
  register_engine_tools(mcp, client)
51
54
  register_hook_tools(mcp, client)
55
+ register_email_template_tools(mcp, client)
52
56
  register_document_relation_tools(mcp, client)
53
57
  register_relation_tools(mcp, client)
54
58
  register_rule_tools(mcp, client)
@@ -3,7 +3,15 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from rossum_mcp.tools.annotations import register_annotation_tools
6
+ from rossum_mcp.tools.catalog import (
7
+ TOOL_CATALOG,
8
+ ToolCategory,
9
+ ToolInfo,
10
+ get_catalog_summary,
11
+ )
12
+ from rossum_mcp.tools.discovery import register_discovery_tools
6
13
  from rossum_mcp.tools.document_relations import register_document_relation_tools
14
+ from rossum_mcp.tools.email_templates import register_email_template_tools
7
15
  from rossum_mcp.tools.engines import register_engine_tools
8
16
  from rossum_mcp.tools.hooks import register_hook_tools
9
17
  from rossum_mcp.tools.queues import register_queue_tools
@@ -14,8 +22,14 @@ from rossum_mcp.tools.users import register_user_tools
14
22
  from rossum_mcp.tools.workspaces import register_workspace_tools
15
23
 
16
24
  __all__ = [
25
+ "TOOL_CATALOG",
26
+ "ToolCategory",
27
+ "ToolInfo",
28
+ "get_catalog_summary",
17
29
  "register_annotation_tools",
30
+ "register_discovery_tools",
18
31
  "register_document_relation_tools",
32
+ "register_email_template_tools",
19
33
  "register_engine_tools",
20
34
  "register_hook_tools",
21
35
  "register_queue_tools",
@@ -7,7 +7,7 @@ from collections.abc import Sequence # noqa: TC003 - needed at runtime for Fast
7
7
  from pathlib import Path
8
8
  from typing import TYPE_CHECKING, Literal
9
9
 
10
- from rossum_api.models.annotation import Annotation # noqa: TC002 - needed at runtime for FastMCP
10
+ from rossum_api.models.annotation import Annotation
11
11
 
12
12
  from rossum_mcp.tools.base import is_read_write_mode
13
13
 
@@ -17,114 +17,151 @@ if TYPE_CHECKING:
17
17
 
18
18
  logger = logging.getLogger(__name__)
19
19
 
20
- # Fixed sideloads (critical for well-behaving agent)
21
20
  type Sideload = Literal["content", "document", "automation_blocker"]
22
21
 
23
22
 
24
- def register_annotation_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None: # noqa: C901
23
+ async def _upload_document(client: AsyncRossumAPIClient, file_path: str, queue_id: int) -> dict:
24
+ if not is_read_write_mode():
25
+ return {"error": "upload_document is not available in read-only mode"}
26
+
27
+ path = Path(file_path)
28
+ if not path.exists():
29
+ logger.error(f"File not found: {file_path}")
30
+ raise FileNotFoundError(f"File not found: {file_path}")
31
+
32
+ try:
33
+ task = (await client.upload_document(queue_id, [(str(path), path.name)]))[0]
34
+ except KeyError as e:
35
+ logger.error(f"Upload failed - unexpected API response format: {e!s}")
36
+ error_msg = (
37
+ f"Document upload failed - API response missing expected key {e!s}. "
38
+ f"This usually means either:\n"
39
+ f"1. The queue_id ({queue_id}) is invalid or you don't have access to it\n"
40
+ f"2. The Rossum API returned an error response\n"
41
+ f"Please verify:\n"
42
+ f"- The queue_id is correct and exists in your workspace\n"
43
+ f"- You have permission to upload documents to this queue\n"
44
+ f"- Your API token has the necessary permissions"
45
+ )
46
+ raise ValueError(error_msg) from e
47
+ except IndexError as e:
48
+ logger.error(f"Upload failed - no tasks returned: {e}")
49
+ raise ValueError(
50
+ f"Document upload failed - no tasks were created. This may indicate the queue_id ({queue_id}) is invalid."
51
+ ) from e
52
+ except Exception as e:
53
+ logger.error(f"Upload failed: {type(e).__name__}: {e}")
54
+ raise ValueError(f"Document upload failed: {type(e).__name__}: {e!s}") from e
55
+
56
+ return {
57
+ "task_id": task.id,
58
+ "task_status": task.status,
59
+ "queue_id": queue_id,
60
+ "message": "Document upload initiated. Use `list_annotations` to find the annotation ID for this queue.",
61
+ }
62
+
63
+
64
+ async def _get_annotation(
65
+ client: AsyncRossumAPIClient, annotation_id: int, sideloads: Sequence[Sideload] = ()
66
+ ) -> Annotation:
67
+ logger.debug(f"Retrieving annotation: annotation_id={annotation_id}")
68
+ annotation: Annotation = await client.retrieve_annotation(annotation_id, sideloads) # type: ignore[arg-type]
69
+ return annotation
70
+
71
+
72
+ async def _list_annotations(
73
+ client: AsyncRossumAPIClient,
74
+ queue_id: int,
75
+ status: str | None = "importing,to_review,confirmed,exported",
76
+ ordering: Sequence[str] = (),
77
+ first_n: int | None = None,
78
+ ) -> list[Annotation]:
79
+ logger.debug(f"Listing annotations: queue_id={queue_id}, status={status}, ordering={ordering}, first_n={first_n}")
80
+ params: dict = {"queue": queue_id, "page_size": 100}
81
+ if status:
82
+ params["status"] = status
83
+ if ordering:
84
+ params["ordering"] = ordering
85
+
86
+ annotations_list: list[Annotation] = []
87
+ async for item in client.list_annotations(**params):
88
+ annotations_list.append(item)
89
+ if first_n is not None and len(annotations_list) >= first_n:
90
+ break
91
+ return annotations_list
92
+
93
+
94
+ async def _start_annotation(client: AsyncRossumAPIClient, annotation_id: int) -> dict:
95
+ if not is_read_write_mode():
96
+ return {"error": "start_annotation is not available in read-only mode"}
97
+
98
+ logger.debug(f"Starting annotation: annotation_id={annotation_id}")
99
+ await client.start_annotation(annotation_id)
100
+ return {
101
+ "annotation_id": annotation_id,
102
+ "message": f"Annotation {annotation_id} started successfully. Status changed to 'reviewing'.",
103
+ }
104
+
105
+
106
+ async def _bulk_update_annotation_fields(
107
+ client: AsyncRossumAPIClient, annotation_id: int, operations: list[dict]
108
+ ) -> dict:
109
+ if not is_read_write_mode():
110
+ return {"error": "bulk_update_annotation_fields is not available in read-only mode"}
111
+
112
+ logger.debug(f"Bulk updating annotation: annotation_id={annotation_id}, ops={operations}")
113
+ await client.bulk_update_annotation_data(annotation_id, operations)
114
+ return {
115
+ "annotation_id": annotation_id,
116
+ "operations_count": len(operations),
117
+ "message": f"Annotation {annotation_id} updated with {len(operations)} operations successfully.",
118
+ }
119
+
120
+
121
+ async def _confirm_annotation(client: AsyncRossumAPIClient, annotation_id: int) -> dict:
122
+ if not is_read_write_mode():
123
+ return {"error": "confirm_annotation is not available in read-only mode"}
124
+
125
+ logger.debug(f"Confirming annotation: annotation_id={annotation_id}")
126
+ await client.confirm_annotation(annotation_id)
127
+ return {
128
+ "annotation_id": annotation_id,
129
+ "message": f"Annotation {annotation_id} confirmed successfully. Status changed to 'confirmed'.",
130
+ }
131
+
132
+
133
+ def register_annotation_tools(mcp: FastMCP, client: AsyncRossumAPIClient) -> None:
25
134
  """Register annotation-related tools with the FastMCP server."""
26
135
 
27
136
  @mcp.tool(description="Upload a document to Rossum. Use list_annotations to get annotation ID.")
28
137
  async def upload_document(file_path: str, queue_id: int) -> dict:
29
- """Upload a document to Rossum."""
30
- if not is_read_write_mode():
31
- return {"error": "upload_document is not available in read-only mode"}
32
-
33
- path = Path(file_path)
34
- if not path.exists():
35
- logger.error(f"File not found: {file_path}")
36
- raise FileNotFoundError(f"File not found: {file_path}")
37
-
38
- try:
39
- task = (await client.upload_document(queue_id, [(str(path), path.name)]))[0]
40
- except KeyError as e:
41
- logger.error(f"Upload failed - unexpected API response format: {e!s}")
42
- error_msg = (
43
- f"Document upload failed - API response missing expected key {e!s}. "
44
- f"This usually means either:\n"
45
- f"1. The queue_id ({queue_id}) is invalid or you don't have access to it\n"
46
- f"2. The Rossum API returned an error response\n"
47
- f"Please verify:\n"
48
- f"- The queue_id is correct and exists in your workspace\n"
49
- f"- You have permission to upload documents to this queue\n"
50
- f"- Your API token has the necessary permissions"
51
- )
52
- raise ValueError(error_msg) from e
53
- except IndexError as e:
54
- logger.error(f"Upload failed - no tasks returned: {e}")
55
- raise ValueError(
56
- f"Document upload failed - no tasks were created. This may indicate the queue_id ({queue_id}) is invalid."
57
- ) from e
58
- except Exception as e:
59
- logger.error(f"Upload failed: {type(e).__name__}: {e}")
60
- raise ValueError(f"Document upload failed: {type(e).__name__}: {e!s}") from e
61
-
62
- return {
63
- "task_id": task.id,
64
- "task_status": task.status,
65
- "queue_id": queue_id,
66
- "message": "Document upload initiated. Use `list_annotations` to find the annotation ID for this queue.",
67
- }
138
+ return await _upload_document(client, file_path, queue_id)
68
139
 
69
140
  @mcp.tool(description="Retrieve annotation data. Use 'content' sideload to get extracted data.")
70
141
  async def get_annotation(annotation_id: int, sideloads: Sequence[Sideload] = ()) -> Annotation:
71
- """Retrieve annotation data from Rossum."""
72
- logger.debug(f"Retrieving annotation: annotation_id={annotation_id}")
73
- annotation: Annotation = await client.retrieve_annotation(annotation_id, sideloads) # type: ignore[arg-type]
74
- return annotation
142
+ return await _get_annotation(client, annotation_id, sideloads)
75
143
 
76
- @mcp.tool(description="List annotations for a queue.")
144
+ @mcp.tool(description="List annotations for a queue. Use ordering=['-created_at'] to sort by newest first.")
77
145
  async def list_annotations(
78
- queue_id: int, status: str | None = "importing,to_review,confirmed,exported"
146
+ queue_id: int,
147
+ status: str | None = "importing,to_review,confirmed,exported",
148
+ ordering: Sequence[str] = (),
149
+ first_n: int | None = None,
79
150
  ) -> list[Annotation]:
80
- """List annotations for a queue with optional filtering."""
81
- logger.debug(f"Listing annotations: queue_id={queue_id}, status={status}")
82
- params: dict = {"queue": queue_id, "page_size": 100}
83
- if status:
84
- params["status"] = status
85
- annotations_list: list[Annotation] = [item async for item in client.list_annotations(**params)]
86
- return annotations_list
151
+ return await _list_annotations(client, queue_id, status, ordering, first_n)
87
152
 
88
153
  @mcp.tool(description="Start annotation (move from 'to_review' to 'reviewing').")
89
154
  async def start_annotation(annotation_id: int) -> dict:
90
- """Start annotation to move it to 'reviewing' status."""
91
- if not is_read_write_mode():
92
- return {"error": "start_annotation is not available in read-only mode"}
93
-
94
- logger.debug(f"Starting annotation: annotation_id={annotation_id}")
95
- await client.start_annotation(annotation_id)
96
- return {
97
- "annotation_id": annotation_id,
98
- "message": f"Annotation {annotation_id} started successfully. Status changed to 'reviewing'.",
99
- }
155
+ return await _start_annotation(client, annotation_id)
100
156
 
101
157
  @mcp.tool(
102
158
  description="Bulk update annotation fields. It can be used after `start_annotation` only. Use datapoint ID from content, NOT schema_id."
103
159
  )
104
160
  async def bulk_update_annotation_fields(annotation_id: int, operations: list[dict]) -> dict:
105
- """Bulk update annotation field values using operations."""
106
- if not is_read_write_mode():
107
- return {"error": "bulk_update_annotation_fields is not available in read-only mode"}
108
-
109
- logger.debug(f"Bulk updating annotation: annotation_id={annotation_id}, ops={operations}")
110
- await client.bulk_update_annotation_data(annotation_id, operations)
111
- return {
112
- "annotation_id": annotation_id,
113
- "operations_count": len(operations),
114
- "message": f"Annotation {annotation_id} updated with {len(operations)} operations successfully.",
115
- }
161
+ return await _bulk_update_annotation_fields(client, annotation_id, operations)
116
162
 
117
163
  @mcp.tool(
118
164
  description="Confirm annotation (move to 'confirmed'). It can be used after `bulk_update_annotation_fields`."
119
165
  )
120
166
  async def confirm_annotation(annotation_id: int) -> dict:
121
- """Confirm annotation to move it to 'confirmed' status."""
122
- if not is_read_write_mode():
123
- return {"error": "confirm_annotation is not available in read-only mode"}
124
-
125
- logger.debug(f"Confirming annotation: annotation_id={annotation_id}")
126
- await client.confirm_annotation(annotation_id)
127
- return {
128
- "annotation_id": annotation_id,
129
- "message": f"Annotation {annotation_id} confirmed successfully. Status changed to 'confirmed'.",
130
- }
167
+ return await _confirm_annotation(client, annotation_id)
rossum_mcp/tools/base.py CHANGED
@@ -3,10 +3,17 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from typing import Any
6
10
 
7
11
  BASE_URL = os.environ.get("ROSSUM_API_BASE_URL", "").rstrip("/")
8
12
  MODE = os.environ.get("ROSSUM_MCP_MODE", "read-write").lower()
9
13
 
14
+ # Marker used to indicate omitted fields in list responses
15
+ TRUNCATED_MARKER = "<omitted>"
16
+
10
17
 
11
18
  def build_resource_url(resource_type: str, resource_id: int) -> str:
12
19
  """Build a full URL for a Rossum API resource."""
@@ -16,3 +23,18 @@ def build_resource_url(resource_type: str, resource_id: int) -> str:
16
23
  def is_read_write_mode() -> bool:
17
24
  """Check if server is in read-write mode."""
18
25
  return MODE == "read-write"
26
+
27
+
28
+ def truncate_dict_fields(data: dict[str, Any], fields: tuple[str, ...]) -> dict[str, Any]:
29
+ """Truncate specified fields in a dictionary to save context.
30
+
31
+ Returns a new dictionary with specified fields replaced by TRUNCATED_MARKER.
32
+ """
33
+ if not data:
34
+ return data
35
+
36
+ result = dict(data)
37
+ for field in fields:
38
+ if field in result:
39
+ result[field] = TRUNCATED_MARKER
40
+ return result
@@ -0,0 +1,169 @@
1
+ """Tool catalog for dynamic tool discovery.
2
+
3
+ Provides lightweight metadata for all MCP tools organized by category.
4
+ This is the single source of truth for tool categorization - the agent
5
+ fetches this catalog from MCP to avoid data duplication.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+
13
+ @dataclass
14
+ class ToolInfo:
15
+ """Lightweight tool metadata for catalog."""
16
+
17
+ name: str
18
+ description: str
19
+
20
+
21
+ @dataclass
22
+ class ToolCategory:
23
+ """A category of related tools."""
24
+
25
+ name: str
26
+ description: str
27
+ tools: list[ToolInfo]
28
+ keywords: list[str]
29
+
30
+
31
+ # Tool catalog organized by functional category
32
+ # Keywords enable automatic pre-loading based on user request text
33
+ TOOL_CATALOG: dict[str, ToolCategory] = {
34
+ "annotations": ToolCategory(
35
+ name="annotations",
36
+ description="Document processing: upload, retrieve, update, and confirm annotations",
37
+ tools=[
38
+ ToolInfo("upload_document", "Upload document to queue"),
39
+ ToolInfo("get_annotation", "Retrieve annotation with extracted data"),
40
+ ToolInfo("list_annotations", "List annotations for a queue"),
41
+ ToolInfo("start_annotation", "Start annotation (to_review -> reviewing)"),
42
+ ToolInfo("bulk_update_annotation_fields", "Bulk update annotation fields"),
43
+ ToolInfo("confirm_annotation", "Confirm annotation (-> confirmed)"),
44
+ ],
45
+ keywords=["annotation", "document", "upload", "extract", "confirm", "review"],
46
+ ),
47
+ "queues": ToolCategory(
48
+ name="queues",
49
+ description="Queue management: create, configure, and list document processing queues",
50
+ tools=[
51
+ ToolInfo("get_queue", "Retrieve queue details"),
52
+ ToolInfo("list_queues", "List all queues"),
53
+ ToolInfo("get_queue_schema", "Get queue's schema"),
54
+ ToolInfo("get_queue_engine", "Get queue's AI engine"),
55
+ ToolInfo("create_queue", "Create a queue"),
56
+ ToolInfo("update_queue", "Update queue settings"),
57
+ ToolInfo("get_queue_template_names", "List available queue templates"),
58
+ ToolInfo("create_queue_from_template", "Create queue from template"),
59
+ ],
60
+ keywords=["queue", "inbox", "connector"],
61
+ ),
62
+ "schemas": ToolCategory(
63
+ name="schemas",
64
+ description="Schema management: define and modify document field structures",
65
+ tools=[
66
+ ToolInfo("get_schema", "Retrieve schema details"),
67
+ ToolInfo("list_schemas", "List all schemas"),
68
+ ToolInfo("update_schema", "Update schema"),
69
+ ToolInfo("create_schema", "Create new schema"),
70
+ ToolInfo("patch_schema", "Add/update/remove schema fields"),
71
+ ToolInfo("get_schema_tree_structure", "Get lightweight schema tree"),
72
+ ToolInfo("prune_schema_fields", "Bulk remove schema fields"),
73
+ ],
74
+ keywords=["schema", "field", "datapoint", "section", "multivalue", "tuple"],
75
+ ),
76
+ "engines": ToolCategory(
77
+ name="engines",
78
+ description="AI engine management: create and configure extraction/splitting engines",
79
+ tools=[
80
+ ToolInfo("get_engine", "Retrieve engine details"),
81
+ ToolInfo("list_engines", "List all engines"),
82
+ ToolInfo("update_engine", "Update engine settings"),
83
+ ToolInfo("create_engine", "Create new engine"),
84
+ ToolInfo("create_engine_field", "Create engine field mapping"),
85
+ ToolInfo("get_engine_fields", "List engine fields"),
86
+ ],
87
+ keywords=["engine", "ai", "extractor", "splitter", "training"],
88
+ ),
89
+ "hooks": ToolCategory(
90
+ name="hooks",
91
+ description="Extensions/webhooks: create and manage automation hooks",
92
+ tools=[
93
+ ToolInfo("get_hook", "Retrieve hook details with code"),
94
+ ToolInfo("list_hooks", "List all hooks for a queue"),
95
+ ToolInfo("create_hook", "Create new hook"),
96
+ ToolInfo("update_hook", "Update hook configuration"),
97
+ ToolInfo("list_hook_logs", "View hook execution logs"),
98
+ ToolInfo("list_hook_templates", "List Rossum Store templates"),
99
+ ToolInfo("create_hook_from_template", "Create hook from template"),
100
+ ],
101
+ keywords=["hook", "extension", "webhook", "automation", "function", "serverless"],
102
+ ),
103
+ "email_templates": ToolCategory(
104
+ name="email_templates",
105
+ description="Email templates: configure automated email responses",
106
+ tools=[
107
+ ToolInfo("get_email_template", "Retrieve email template"),
108
+ ToolInfo("list_email_templates", "List email templates"),
109
+ ToolInfo("create_email_template", "Create email template"),
110
+ ],
111
+ keywords=["email", "template", "notification", "rejection"],
112
+ ),
113
+ "document_relations": ToolCategory(
114
+ name="document_relations",
115
+ description="Document relations: manage export/einvoice document links",
116
+ tools=[
117
+ ToolInfo("get_document_relation", "Retrieve document relation"),
118
+ ToolInfo("list_document_relations", "List document relations"),
119
+ ],
120
+ keywords=["document relation", "export", "einvoice"],
121
+ ),
122
+ "relations": ToolCategory(
123
+ name="relations",
124
+ description="Annotation relations: manage edit/attachment/duplicate links",
125
+ tools=[
126
+ ToolInfo("get_relation", "Retrieve relation details"),
127
+ ToolInfo("list_relations", "List annotation relations"),
128
+ ],
129
+ keywords=["relation", "duplicate", "attachment", "edit"],
130
+ ),
131
+ "rules": ToolCategory(
132
+ name="rules",
133
+ description="Validation rules: manage schema validation rules",
134
+ tools=[
135
+ ToolInfo("get_rule", "Retrieve rule details"),
136
+ ToolInfo("list_rules", "List validation rules"),
137
+ ],
138
+ keywords=["rule", "validation", "constraint"],
139
+ ),
140
+ "users": ToolCategory(
141
+ name="users",
142
+ description="User management: list users and roles",
143
+ tools=[
144
+ ToolInfo("get_user", "Retrieve user details"),
145
+ ToolInfo("list_users", "List users with filters"),
146
+ ToolInfo("list_user_roles", "List available user roles"),
147
+ ],
148
+ keywords=["user", "role", "permission", "token_owner"],
149
+ ),
150
+ "workspaces": ToolCategory(
151
+ name="workspaces",
152
+ description="Workspace management: organize queues into workspaces",
153
+ tools=[
154
+ ToolInfo("get_workspace", "Retrieve workspace details"),
155
+ ToolInfo("list_workspaces", "List all workspaces"),
156
+ ToolInfo("create_workspace", "Create new workspace"),
157
+ ],
158
+ keywords=["workspace", "organization"],
159
+ ),
160
+ }
161
+
162
+
163
+ def get_catalog_summary() -> str:
164
+ """Get a compact text summary of all tool categories for the system prompt."""
165
+ lines = ["Available MCP tool categories (use `list_tool_categories` for details):"]
166
+ for category in TOOL_CATALOG.values():
167
+ tool_names = ", ".join(t.name for t in category.tools)
168
+ lines.append(f"- **{category.name}**: {category.description} [{tool_names}]")
169
+ return "\n".join(lines)
@@ -0,0 +1,36 @@
1
+ """Discovery tools for dynamic tool loading.
2
+
3
+ Provides MCP tool to explore available tool categories and their metadata.
4
+ The agent uses this to fetch the catalog and load tools on-demand.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import asdict
10
+ from typing import TYPE_CHECKING
11
+
12
+ from rossum_mcp.tools.catalog import TOOL_CATALOG
13
+
14
+ if TYPE_CHECKING:
15
+ from fastmcp import FastMCP
16
+
17
+
18
+ def register_discovery_tools(mcp: FastMCP) -> None:
19
+ """Register discovery tools with the FastMCP server."""
20
+
21
+ @mcp.tool(
22
+ description="List all available tool categories with descriptions, tool names, and keywords. "
23
+ "Use this to discover what tools are available, then use load_tool_category to load "
24
+ "tools from specific categories before using them."
25
+ )
26
+ async def list_tool_categories() -> list[dict]:
27
+ return [
28
+ {
29
+ "name": category.name,
30
+ "description": category.description,
31
+ "tool_count": len(category.tools),
32
+ "tools": [asdict(tool) for tool in category.tools],
33
+ "keywords": category.keywords,
34
+ }
35
+ for category in TOOL_CATALOG.values()
36
+ ]
@@ -5,9 +5,7 @@ from __future__ import annotations
5
5
  import logging
6
6
  from typing import TYPE_CHECKING
7
7
 
8
- from rossum_api.models.document_relation import (
9
- DocumentRelation, # noqa: TC002 - needed at runtime for FastMCP
10
- )
8
+ from rossum_api.models.document_relation import DocumentRelation
11
9
 
12
10
  if TYPE_CHECKING:
13
11
  from fastmcp import FastMCP