iflow-mcp_democratize-technology-chronos-mcp 2.0.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.
- chronos_mcp/__init__.py +5 -0
- chronos_mcp/__main__.py +9 -0
- chronos_mcp/accounts.py +410 -0
- chronos_mcp/bulk.py +946 -0
- chronos_mcp/caldav_utils.py +149 -0
- chronos_mcp/calendars.py +204 -0
- chronos_mcp/config.py +187 -0
- chronos_mcp/credentials.py +190 -0
- chronos_mcp/events.py +515 -0
- chronos_mcp/exceptions.py +477 -0
- chronos_mcp/journals.py +477 -0
- chronos_mcp/logging_config.py +23 -0
- chronos_mcp/models.py +202 -0
- chronos_mcp/py.typed +0 -0
- chronos_mcp/rrule.py +259 -0
- chronos_mcp/search.py +315 -0
- chronos_mcp/server.py +121 -0
- chronos_mcp/tasks.py +518 -0
- chronos_mcp/tools/__init__.py +29 -0
- chronos_mcp/tools/accounts.py +151 -0
- chronos_mcp/tools/base.py +59 -0
- chronos_mcp/tools/bulk.py +557 -0
- chronos_mcp/tools/calendars.py +142 -0
- chronos_mcp/tools/events.py +698 -0
- chronos_mcp/tools/journals.py +310 -0
- chronos_mcp/tools/tasks.py +414 -0
- chronos_mcp/utils.py +163 -0
- chronos_mcp/validation.py +636 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/METADATA +299 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/RECORD +68 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/WHEEL +5 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/conftest.py +91 -0
- tests/unit/__init__.py +0 -0
- tests/unit/test_accounts.py +380 -0
- tests/unit/test_accounts_ssrf.py +134 -0
- tests/unit/test_base.py +135 -0
- tests/unit/test_bulk.py +380 -0
- tests/unit/test_bulk_create.py +408 -0
- tests/unit/test_bulk_delete.py +341 -0
- tests/unit/test_bulk_resource_limits.py +74 -0
- tests/unit/test_caldav_utils.py +300 -0
- tests/unit/test_calendars.py +286 -0
- tests/unit/test_config.py +111 -0
- tests/unit/test_config_validation.py +128 -0
- tests/unit/test_credentials_security.py +189 -0
- tests/unit/test_cryptography_security.py +178 -0
- tests/unit/test_events.py +536 -0
- tests/unit/test_exceptions.py +58 -0
- tests/unit/test_journals.py +1097 -0
- tests/unit/test_models.py +95 -0
- tests/unit/test_race_conditions.py +202 -0
- tests/unit/test_recurring_events.py +156 -0
- tests/unit/test_rrule.py +217 -0
- tests/unit/test_search.py +372 -0
- tests/unit/test_search_advanced.py +333 -0
- tests/unit/test_server_input_validation.py +219 -0
- tests/unit/test_ssrf_protection.py +505 -0
- tests/unit/test_tasks.py +918 -0
- tests/unit/test_thread_safety.py +301 -0
- tests/unit/test_tools_journals.py +617 -0
- tests/unit/test_tools_tasks.py +968 -0
- tests/unit/test_url_validation_security.py +234 -0
- tests/unit/test_utils.py +180 -0
- tests/unit/test_validation.py +983 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Journal management tools for Chronos MCP
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import Any, Dict, List, Optional, Union
|
|
7
|
+
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from ..exceptions import (
|
|
11
|
+
CalendarNotFoundError,
|
|
12
|
+
ChronosError,
|
|
13
|
+
ErrorSanitizer,
|
|
14
|
+
EventNotFoundError,
|
|
15
|
+
ValidationError,
|
|
16
|
+
)
|
|
17
|
+
from ..logging_config import setup_logging
|
|
18
|
+
from ..utils import parse_datetime
|
|
19
|
+
from ..validation import InputValidator
|
|
20
|
+
from .base import create_success_response, handle_tool_errors
|
|
21
|
+
|
|
22
|
+
logger = setup_logging()
|
|
23
|
+
|
|
24
|
+
# Module-level managers dictionary for dependency injection
|
|
25
|
+
_managers = {}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Journal tool functions - defined as standalone functions for importability
|
|
29
|
+
async def create_journal(
|
|
30
|
+
calendar_uid: str = Field(..., description="Calendar UID"),
|
|
31
|
+
summary: str = Field(..., description="Journal entry title/summary"),
|
|
32
|
+
description: Optional[str] = Field(None, description="Journal entry content"),
|
|
33
|
+
entry_date: Optional[str] = Field(
|
|
34
|
+
None, description="Journal entry date (ISO format)"
|
|
35
|
+
),
|
|
36
|
+
related_to: Optional[List[str]] = Field(
|
|
37
|
+
None, description="List of related component UIDs"
|
|
38
|
+
),
|
|
39
|
+
account: Optional[str] = Field(None, description="Account alias"),
|
|
40
|
+
) -> Dict[str, Any]:
|
|
41
|
+
"""Create a new journal entry"""
|
|
42
|
+
request_id = str(uuid.uuid4())
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
# Validate and sanitize text inputs
|
|
46
|
+
try:
|
|
47
|
+
summary = InputValidator.validate_text_field(
|
|
48
|
+
summary, "summary", required=True
|
|
49
|
+
)
|
|
50
|
+
if description:
|
|
51
|
+
description = InputValidator.validate_text_field(
|
|
52
|
+
description, "description"
|
|
53
|
+
)
|
|
54
|
+
except ValidationError as e:
|
|
55
|
+
return {
|
|
56
|
+
"success": False,
|
|
57
|
+
"error": str(e),
|
|
58
|
+
"error_code": "VALIDATION_ERROR",
|
|
59
|
+
"request_id": request_id,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Parse entry date if provided
|
|
63
|
+
entry_dt = None
|
|
64
|
+
if entry_date:
|
|
65
|
+
entry_dt = parse_datetime(entry_date)
|
|
66
|
+
|
|
67
|
+
journal = _managers["journal_manager"].create_journal(
|
|
68
|
+
calendar_uid=calendar_uid,
|
|
69
|
+
summary=summary,
|
|
70
|
+
description=description,
|
|
71
|
+
dtstart=entry_dt,
|
|
72
|
+
related_to=related_to,
|
|
73
|
+
account_alias=account,
|
|
74
|
+
request_id=request_id,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
"success": True,
|
|
79
|
+
"journal": {
|
|
80
|
+
"uid": journal.uid,
|
|
81
|
+
"summary": journal.summary,
|
|
82
|
+
"description": journal.description,
|
|
83
|
+
"entry_date": (
|
|
84
|
+
journal.dtstart.isoformat() if journal.dtstart else None
|
|
85
|
+
),
|
|
86
|
+
"related_to": journal.related_to,
|
|
87
|
+
},
|
|
88
|
+
"request_id": request_id,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
except ChronosError as e:
|
|
92
|
+
e.request_id = request_id
|
|
93
|
+
logger.error(f"Create journal failed: {e}")
|
|
94
|
+
return {
|
|
95
|
+
"success": False,
|
|
96
|
+
"error": ErrorSanitizer.get_user_friendly_message(e),
|
|
97
|
+
"error_code": e.error_code,
|
|
98
|
+
"request_id": request_id,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
chronos_error = ChronosError(
|
|
103
|
+
message=f"Failed to create journal: {str(e)}",
|
|
104
|
+
details={
|
|
105
|
+
"tool": "create_journal",
|
|
106
|
+
"summary": summary,
|
|
107
|
+
"calendar_uid": calendar_uid,
|
|
108
|
+
"original_error": str(e),
|
|
109
|
+
"original_type": type(e).__name__,
|
|
110
|
+
},
|
|
111
|
+
request_id=request_id,
|
|
112
|
+
)
|
|
113
|
+
logger.error(f"Unexpected error in create_journal: {chronos_error}")
|
|
114
|
+
return {
|
|
115
|
+
"success": False,
|
|
116
|
+
"error": ErrorSanitizer.get_user_friendly_message(chronos_error),
|
|
117
|
+
"error_code": chronos_error.error_code,
|
|
118
|
+
"request_id": request_id,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def list_journals(
|
|
123
|
+
calendar_uid: str = Field(..., description="Calendar UID"),
|
|
124
|
+
account: Optional[str] = Field(None, description="Account alias"),
|
|
125
|
+
limit: Optional[Union[int, str]] = Field(
|
|
126
|
+
50, description="Maximum number of journals to return"
|
|
127
|
+
),
|
|
128
|
+
) -> Dict[str, Any]:
|
|
129
|
+
"""List journal entries in a calendar"""
|
|
130
|
+
request_id = str(uuid.uuid4())
|
|
131
|
+
|
|
132
|
+
# Handle type conversion for limit parameter
|
|
133
|
+
if limit is not None:
|
|
134
|
+
try:
|
|
135
|
+
limit = int(limit)
|
|
136
|
+
except (ValueError, TypeError):
|
|
137
|
+
return {
|
|
138
|
+
"journals": [],
|
|
139
|
+
"total": 0,
|
|
140
|
+
"error": f"Invalid limit value: {limit}. Must be an integer",
|
|
141
|
+
"error_code": "VALIDATION_ERROR",
|
|
142
|
+
"request_id": request_id,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
journals = _managers["journal_manager"].list_journals(
|
|
147
|
+
calendar_uid=calendar_uid,
|
|
148
|
+
limit=limit,
|
|
149
|
+
account_alias=account,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
"journals": [
|
|
154
|
+
{
|
|
155
|
+
"uid": journal.uid,
|
|
156
|
+
"summary": journal.summary,
|
|
157
|
+
"description": journal.description,
|
|
158
|
+
"entry_date": (
|
|
159
|
+
journal.dtstart.isoformat() if journal.dtstart else None
|
|
160
|
+
),
|
|
161
|
+
"related_to": journal.related_to,
|
|
162
|
+
}
|
|
163
|
+
for journal in journals
|
|
164
|
+
],
|
|
165
|
+
"total": len(journals),
|
|
166
|
+
"calendar_uid": calendar_uid,
|
|
167
|
+
"request_id": request_id,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
except CalendarNotFoundError as e:
|
|
171
|
+
e.request_id = request_id
|
|
172
|
+
logger.error(f"Calendar not found for journal listing: {e}")
|
|
173
|
+
return {
|
|
174
|
+
"journals": [],
|
|
175
|
+
"total": 0,
|
|
176
|
+
"error": ErrorSanitizer.get_user_friendly_message(e),
|
|
177
|
+
"error_code": e.error_code,
|
|
178
|
+
"request_id": request_id,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
except ChronosError as e:
|
|
182
|
+
e.request_id = request_id
|
|
183
|
+
logger.error(f"List journals failed: {e}")
|
|
184
|
+
return {
|
|
185
|
+
"journals": [],
|
|
186
|
+
"total": 0,
|
|
187
|
+
"error": ErrorSanitizer.get_user_friendly_message(e),
|
|
188
|
+
"error_code": e.error_code,
|
|
189
|
+
"request_id": request_id,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
chronos_error = ChronosError(
|
|
194
|
+
message=f"Failed to list journals: {str(e)}",
|
|
195
|
+
details={
|
|
196
|
+
"tool": "list_journals",
|
|
197
|
+
"calendar_uid": calendar_uid,
|
|
198
|
+
"original_error": str(e),
|
|
199
|
+
"original_type": type(e).__name__,
|
|
200
|
+
},
|
|
201
|
+
request_id=request_id,
|
|
202
|
+
)
|
|
203
|
+
logger.error(f"Unexpected error in list_journals: {chronos_error}")
|
|
204
|
+
return {
|
|
205
|
+
"journals": [],
|
|
206
|
+
"total": 0,
|
|
207
|
+
"error": ErrorSanitizer.get_user_friendly_message(chronos_error),
|
|
208
|
+
"error_code": chronos_error.error_code,
|
|
209
|
+
"request_id": request_id,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@handle_tool_errors
|
|
214
|
+
async def update_journal(
|
|
215
|
+
calendar_uid: str = Field(..., description="Calendar UID"),
|
|
216
|
+
journal_uid: str = Field(..., description="Journal UID to update"),
|
|
217
|
+
summary: Optional[str] = Field(None, description="Journal entry title/summary"),
|
|
218
|
+
description: Optional[str] = Field(None, description="Journal entry content"),
|
|
219
|
+
entry_date: Optional[str] = Field(
|
|
220
|
+
None, description="Journal entry date (ISO format)"
|
|
221
|
+
),
|
|
222
|
+
account: Optional[str] = Field(None, description="Account alias"),
|
|
223
|
+
request_id: str = None,
|
|
224
|
+
) -> Dict[str, Any]:
|
|
225
|
+
"""Update an existing journal entry. Only provided fields will be updated."""
|
|
226
|
+
# Validate inputs
|
|
227
|
+
if summary is not None:
|
|
228
|
+
summary = InputValidator.validate_text_field(summary, "summary", required=True)
|
|
229
|
+
if description is not None:
|
|
230
|
+
description = InputValidator.validate_text_field(description, "description")
|
|
231
|
+
|
|
232
|
+
# Parse entry date if provided
|
|
233
|
+
entry_dt = None
|
|
234
|
+
if entry_date is not None:
|
|
235
|
+
entry_dt = parse_datetime(entry_date)
|
|
236
|
+
|
|
237
|
+
updated_journal = _managers["journal_manager"].update_journal(
|
|
238
|
+
calendar_uid=calendar_uid,
|
|
239
|
+
journal_uid=journal_uid,
|
|
240
|
+
summary=summary,
|
|
241
|
+
description=description,
|
|
242
|
+
dtstart=entry_dt,
|
|
243
|
+
account_alias=account,
|
|
244
|
+
request_id=request_id,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return create_success_response(
|
|
248
|
+
message=f"Journal '{journal_uid}' updated successfully",
|
|
249
|
+
request_id=request_id,
|
|
250
|
+
journal={
|
|
251
|
+
"uid": updated_journal.uid,
|
|
252
|
+
"summary": updated_journal.summary,
|
|
253
|
+
"description": updated_journal.description,
|
|
254
|
+
"entry_date": (
|
|
255
|
+
updated_journal.dtstart.isoformat() if updated_journal.dtstart else None
|
|
256
|
+
),
|
|
257
|
+
"related_to": updated_journal.related_to,
|
|
258
|
+
},
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@handle_tool_errors
|
|
263
|
+
async def delete_journal(
|
|
264
|
+
calendar_uid: str = Field(..., description="Calendar UID"),
|
|
265
|
+
journal_uid: str = Field(..., description="Journal UID to delete"),
|
|
266
|
+
account: Optional[str] = Field(None, description="Account alias"),
|
|
267
|
+
request_id: str = None,
|
|
268
|
+
) -> Dict[str, Any]:
|
|
269
|
+
"""Delete a journal entry"""
|
|
270
|
+
_managers["journal_manager"].delete_journal(
|
|
271
|
+
calendar_uid=calendar_uid,
|
|
272
|
+
journal_uid=journal_uid,
|
|
273
|
+
account_alias=account,
|
|
274
|
+
request_id=request_id,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
return create_success_response(
|
|
278
|
+
message=f"Journal '{journal_uid}' deleted successfully",
|
|
279
|
+
request_id=request_id,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def register_journal_tools(mcp, managers):
|
|
284
|
+
"""Register journal management tools with the MCP server"""
|
|
285
|
+
|
|
286
|
+
# Update module-level managers for dependency injection
|
|
287
|
+
_managers.update(managers)
|
|
288
|
+
|
|
289
|
+
# Register all journal tools with the MCP server
|
|
290
|
+
mcp.tool(create_journal)
|
|
291
|
+
mcp.tool(list_journals)
|
|
292
|
+
mcp.tool(update_journal)
|
|
293
|
+
mcp.tool(delete_journal)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# Add .fn attribute to each function for backwards compatibility with tests
|
|
297
|
+
create_journal.fn = create_journal
|
|
298
|
+
list_journals.fn = list_journals
|
|
299
|
+
update_journal.fn = update_journal
|
|
300
|
+
delete_journal.fn = delete_journal
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# Export all tools for backwards compatibility
|
|
304
|
+
__all__ = [
|
|
305
|
+
"create_journal",
|
|
306
|
+
"list_journals",
|
|
307
|
+
"update_journal",
|
|
308
|
+
"delete_journal",
|
|
309
|
+
"register_journal_tools",
|
|
310
|
+
]
|